flowmind 1.1.0 → 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.
- package/bin/flowmind.js +454 -0
- package/core/event-bus.js +17 -0
- package/core/honor-engine.js +255 -0
- package/core/index.js +42 -3
- package/core/learning-engine.js +29 -1
- package/core/skill-loader.js +9 -1
- package/dashboard/app.jsx +29 -0
- package/dashboard/components/ActivityFeed.jsx +86 -0
- package/dashboard/components/DragonPanel.jsx +67 -0
- package/dashboard/components/McpStatusBar.jsx +43 -0
- package/dashboard/components/StatsRow.jsx +65 -0
- package/mcp/server.js +63 -48
- package/package.json +11 -5
- package/tui/app.jsx +69 -0
- package/tui/components/ChatPanel.jsx +72 -0
- package/tui/components/DragonTotem.jsx +108 -0
- package/tui/components/ResultPanel.jsx +33 -0
- package/tui/components/Sidebar.jsx +70 -0
- package/tui/components/StatusBar.jsx +49 -0
|
@@ -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,18 +5,21 @@
|
|
|
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');
|
|
11
12
|
const ModelManager = require('./ai/model-manager');
|
|
13
|
+
const eventBus = require('./event-bus');
|
|
12
14
|
|
|
13
15
|
class FlowMind {
|
|
14
16
|
constructor(options = {}) {
|
|
15
17
|
this.config = new ConfigManager(options.configPath);
|
|
16
|
-
this.
|
|
18
|
+
this.honor = new HonorEngine(this.config);
|
|
19
|
+
this.learning = new LearningEngine(this.config, this.honor);
|
|
17
20
|
this.matcher = new SceneMatcher(this.config, this.learning);
|
|
18
21
|
this.components = new ComponentRegistry(this.config);
|
|
19
|
-
this.skills = new SkillLoader(this.config, this.learning, this.components);
|
|
22
|
+
this.skills = new SkillLoader(this.config, this.learning, this.components, this.honor);
|
|
20
23
|
this.ai = new ModelManager(options.ai || {});
|
|
21
24
|
this.initialized = false;
|
|
22
25
|
}
|
|
@@ -30,6 +33,7 @@ class FlowMind {
|
|
|
30
33
|
await this.config.load();
|
|
31
34
|
await this.components.init();
|
|
32
35
|
await this.components.initAll();
|
|
36
|
+
await this.honor.init();
|
|
33
37
|
await this.learning.init();
|
|
34
38
|
await this.skills.loadAll();
|
|
35
39
|
await this.matcher.loadScenes();
|
|
@@ -55,6 +59,8 @@ class FlowMind {
|
|
|
55
59
|
|
|
56
60
|
const startTime = Date.now();
|
|
57
61
|
|
|
62
|
+
eventBus.emit('process:start', { input, timestamp: new Date().toISOString() });
|
|
63
|
+
|
|
58
64
|
try {
|
|
59
65
|
// 1. AI Intent Understanding (if available)
|
|
60
66
|
const intent = await this.ai.understandIntent(input, context);
|
|
@@ -114,7 +120,7 @@ class FlowMind {
|
|
|
114
120
|
});
|
|
115
121
|
|
|
116
122
|
// 8. Format and return
|
|
117
|
-
|
|
123
|
+
const formatted = this.formatResult(summary || result, {
|
|
118
124
|
skill: skill.name,
|
|
119
125
|
duration: Date.now() - startTime,
|
|
120
126
|
sceneMatch: sceneMatch,
|
|
@@ -122,7 +128,18 @@ class FlowMind {
|
|
|
122
128
|
aiEnhanced: !!summary
|
|
123
129
|
});
|
|
124
130
|
|
|
131
|
+
eventBus.emit('process:complete', {
|
|
132
|
+
input,
|
|
133
|
+
skill: skill.name,
|
|
134
|
+
duration: formatted.metadata.duration,
|
|
135
|
+
success: true,
|
|
136
|
+
timestamp: formatted.timestamp
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return formatted;
|
|
140
|
+
|
|
125
141
|
} catch (error) {
|
|
142
|
+
eventBus.emit('process:error', { input, error: error.message, timestamp: new Date().toISOString() });
|
|
126
143
|
return this.formatError(error.message, input);
|
|
127
144
|
}
|
|
128
145
|
}
|
|
@@ -131,6 +148,8 @@ class FlowMind {
|
|
|
131
148
|
* Execute skill with learning applied
|
|
132
149
|
*/
|
|
133
150
|
async executeWithLearning(skill, input, context) {
|
|
151
|
+
const skillStartTime = Date.now();
|
|
152
|
+
|
|
134
153
|
// Get learning rules for this skill
|
|
135
154
|
const learnings = await this.learning.getSkillLearnings(skill.name);
|
|
136
155
|
|
|
@@ -144,6 +163,19 @@ class FlowMind {
|
|
|
144
163
|
// Execute skill
|
|
145
164
|
const result = await skill.execute(input, enhancedContext);
|
|
146
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
|
+
|
|
147
179
|
return result;
|
|
148
180
|
}
|
|
149
181
|
|
|
@@ -227,6 +259,13 @@ class FlowMind {
|
|
|
227
259
|
return this.learning.getStats();
|
|
228
260
|
}
|
|
229
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Get honor data
|
|
264
|
+
*/
|
|
265
|
+
getHonorData() {
|
|
266
|
+
return this.honor.getData();
|
|
267
|
+
}
|
|
268
|
+
|
|
230
269
|
/**
|
|
231
270
|
* Get the component registry
|
|
232
271
|
* @returns {ComponentRegistry}
|
package/core/learning-engine.js
CHANGED
|
@@ -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
|
/**
|
package/core/skill-loader.js
CHANGED
|
@@ -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);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, useApp, useInput } = require('ink');
|
|
3
|
+
const ActivityFeed = require('./components/ActivityFeed.jsx');
|
|
4
|
+
const StatsRow = require('./components/StatsRow.jsx');
|
|
5
|
+
const DragonPanel = require('./components/DragonPanel.jsx');
|
|
6
|
+
const McpStatusBar = require('./components/McpStatusBar.jsx');
|
|
7
|
+
|
|
8
|
+
function DashboardApp({ flowmind, eventBus }) {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (key.ctrl && input === 'c') exit();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
17
|
+
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
18
|
+
React.createElement(ActivityFeed, { eventBus: eventBus }),
|
|
19
|
+
React.createElement(Box, { flexDirection: 'column', width: '60%', flexGrow: 1 },
|
|
20
|
+
React.createElement(StatsRow, { flowmind: flowmind }),
|
|
21
|
+
React.createElement(DragonPanel, { flowmind: flowmind })
|
|
22
|
+
)
|
|
23
|
+
),
|
|
24
|
+
React.createElement(McpStatusBar, { eventBus: eventBus })
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = DashboardApp;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
const EVENT_COLORS = {
|
|
5
|
+
'skill:executed': 'green',
|
|
6
|
+
'honor:awarded': 'yellow',
|
|
7
|
+
'learning:recorded': 'cyan',
|
|
8
|
+
'mcp:tool_called': 'magenta',
|
|
9
|
+
'process:start': 'blue',
|
|
10
|
+
'process:complete': 'green',
|
|
11
|
+
'process:error': 'red',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function formatTime(timestamp) {
|
|
15
|
+
if (!timestamp) return '??:??';
|
|
16
|
+
return new Date(timestamp).toTimeString().substring(0, 8);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatEvent(event) {
|
|
20
|
+
switch (event.type) {
|
|
21
|
+
case 'skill:executed':
|
|
22
|
+
return 'skill:' + (event.data?.name || '?') + ' ' + (event.data?.success ? '\u2713' : '\u2717');
|
|
23
|
+
case 'honor:awarded':
|
|
24
|
+
return 'honor +' + (event.data?.points || 0) + ' (' + (event.data?.description || '') + ')';
|
|
25
|
+
case 'learning:recorded':
|
|
26
|
+
return 'learning:' + (event.data?.type || '?') + ' ' + (event.data?.skill || '');
|
|
27
|
+
case 'mcp:tool_called':
|
|
28
|
+
return 'MCP:' + (event.data?.tool || '?') + ' ' + (event.data?.success ? '\u2713' : '\u2717') + ' ' + (event.data?.duration || 0) + 'ms';
|
|
29
|
+
case 'process:start':
|
|
30
|
+
return 'process: ' + (event.data?.input?.substring(0, 30) || '?') + '...';
|
|
31
|
+
case 'process:complete':
|
|
32
|
+
return 'done:' + (event.data?.skill || '?') + ' ' + (event.data?.duration || 0) + 'ms';
|
|
33
|
+
case 'process:error':
|
|
34
|
+
return 'error: ' + (event.data?.error?.substring(0, 40) || '?');
|
|
35
|
+
default:
|
|
36
|
+
return event.type || 'unknown';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ActivityFeed({ eventBus }) {
|
|
41
|
+
const [events, setEvents] = React.useState([]);
|
|
42
|
+
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (!eventBus) return;
|
|
45
|
+
|
|
46
|
+
const handler = (eventType) => (data) => {
|
|
47
|
+
setEvents(prev => {
|
|
48
|
+
const next = [...prev, { type: eventType, data, timestamp: data.timestamp || new Date().toISOString() }];
|
|
49
|
+
if (next.length > 100) next.shift();
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const eventTypes = ['skill:executed', 'honor:awarded', 'learning:recorded', 'mcp:tool_called', 'process:start', 'process:complete', 'process:error'];
|
|
55
|
+
const handlers = eventTypes.map(type => {
|
|
56
|
+
const h = handler(type);
|
|
57
|
+
eventBus.on(type, h);
|
|
58
|
+
return { type, handler: h };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
for (const { type, handler: h } of handlers) {
|
|
63
|
+
eventBus.removeListener(type, h);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [eventBus]);
|
|
67
|
+
|
|
68
|
+
const displayEvents = events.slice(-30);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'green', paddingX: 1, width: '40%' },
|
|
72
|
+
React.createElement(Text, { bold: true, color: 'green' }, 'Activity Feed'),
|
|
73
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1, overflow: 'hidden' },
|
|
74
|
+
displayEvents.length === 0 && React.createElement(Text, { color: 'gray' }, 'Waiting for events...'),
|
|
75
|
+
displayEvents.map((event, i) =>
|
|
76
|
+
React.createElement(Text, { key: i },
|
|
77
|
+
React.createElement(Text, { color: 'gray' }, formatTime(event.timestamp) + ' '),
|
|
78
|
+
React.createElement(Text, { color: EVENT_COLORS[event.type] || 'white' }, formatEvent(event))
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = ActivityFeed;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
const DRAGON_ARTS = {
|
|
5
|
+
0: [' ╭─────╮ ',' ╱ ╭─╮ ╲ ',' │ │ │ │ ',' │ │ ◎ │ │ ',' │ ╰─╯ │ ',' ╲ ╱ ',' ╰─────╯ '],
|
|
6
|
+
1: [' ╭──╮ ',' ╭────╯ ╰───╮ ',' ╱ ◎ ╰─╯ ╲ ',' ╱ ▽ ╲ ',' ╲ ╱╲ ╱╲ ╱ ',' ╲╱╱ ╲╱╱ ╲╱╲╱ '],
|
|
7
|
+
2: [' ╭─╮ ╭─╮ ',' ╭────╯ ╰──╯ ╰───╮ ',' ╱ ◎ ╰──╯ ╲ ',' ╱ ╭────────╮ ╲ ',' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',' ╰─╯ ╰─╯ '],
|
|
8
|
+
3: [' ╭───╮ ╭───╮ ',' ╭───╯ ╰──╯ ╰───╮ ',' ╱ ◎ ╰───╯ ╲ ','│ ╭──────────╮ │ ','│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',' ╰───╯ ╰───╯ '],
|
|
9
|
+
4: [' ╭───╮ ╭───╮ ','╭───╯ ╰──────╯ ╰───╮ ','│ ◎ ╰───╯ │ ','│ ╭────────────╮ │ ','│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',' ╰───╯ ╰───╯ '],
|
|
10
|
+
5: [' ★ ╭───╮ ╭───╮ ★ ','╭─╯ ╰──╯ ╰──╯ ╰─╮ ','│ ◎ ╰───╯ │ ','│ ╭──────────────╮ │ ','│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',' ╰───╯ ╰───╯ '],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
|
|
14
|
+
const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
|
|
15
|
+
const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
|
|
16
|
+
|
|
17
|
+
function DragonPanel({ flowmind }) {
|
|
18
|
+
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (!flowmind) return;
|
|
22
|
+
const refresh = () => {
|
|
23
|
+
try { setHonorData(flowmind.getHonorData()); } catch (e) { /* ignore */ }
|
|
24
|
+
};
|
|
25
|
+
refresh();
|
|
26
|
+
const interval = setInterval(refresh, 5000);
|
|
27
|
+
return () => clearInterval(interval);
|
|
28
|
+
}, [flowmind]);
|
|
29
|
+
|
|
30
|
+
const level = honorData.level || 0;
|
|
31
|
+
const art = DRAGON_ARTS[level] || DRAGON_ARTS[0];
|
|
32
|
+
const color = LEVEL_COLORS[level] || 'gray';
|
|
33
|
+
const levelName = LEVEL_NAMES[level] || 'Unknown';
|
|
34
|
+
const state = LEVEL_STATES[level] || 'unknown';
|
|
35
|
+
const nextLevelPoints = [1, 10, 30, 60, 100];
|
|
36
|
+
const nextPoints = nextLevelPoints[level] || null;
|
|
37
|
+
const pointsToNext = nextPoints !== null ? nextPoints - honorData.points : 0;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1, flexGrow: 1 },
|
|
41
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
|
|
42
|
+
React.createElement(Box, { flexDirection: 'row', marginTop: 1 },
|
|
43
|
+
React.createElement(Box, { flexDirection: 'column' },
|
|
44
|
+
art.map((line, i) => React.createElement(Text, { key: i, color: color }, line))
|
|
45
|
+
),
|
|
46
|
+
React.createElement(Box, { flexDirection: 'column', marginLeft: 3, justifyContent: 'center' },
|
|
47
|
+
React.createElement(Text, null,
|
|
48
|
+
React.createElement(Text, { color: 'yellow', bold: true }, 'Lv' + level),
|
|
49
|
+
React.createElement(Text, { color: 'white' }, ' ' + levelName)
|
|
50
|
+
),
|
|
51
|
+
React.createElement(Text, { color: 'gray' }, 'State: ' + state),
|
|
52
|
+
React.createElement(Text, null,
|
|
53
|
+
React.createElement(Text, { color: 'yellow' }, '' + honorData.points),
|
|
54
|
+
React.createElement(Text, { color: 'gray' }, ' points')
|
|
55
|
+
),
|
|
56
|
+
pointsToNext > 0 && React.createElement(Text, { color: 'gray' }, pointsToNext + ' to next'),
|
|
57
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
58
|
+
React.createElement(Text, { color: 'gray' }, 'Skills: ' + (honorData.stats?.skillUseCount || 0)),
|
|
59
|
+
React.createElement(Text, { color: 'gray' }, 'Learnings: ' + (honorData.stats?.learningCount || 0))
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = DragonPanel;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
function McpStatusBar({ eventBus }) {
|
|
5
|
+
const [toolCount, setToolCount] = React.useState(0);
|
|
6
|
+
const [lastCall, setLastCall] = React.useState(null);
|
|
7
|
+
const [serverState, setServerState] = React.useState('running');
|
|
8
|
+
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
if (!eventBus) return;
|
|
11
|
+
const handler = (data) => {
|
|
12
|
+
setToolCount(prev => prev + 1);
|
|
13
|
+
setLastCall(data.timestamp || new Date().toISOString());
|
|
14
|
+
};
|
|
15
|
+
eventBus.on('mcp:tool_called', handler);
|
|
16
|
+
return () => { eventBus.removeListener('mcp:tool_called', handler); };
|
|
17
|
+
}, [eventBus]);
|
|
18
|
+
|
|
19
|
+
const formatTime = (ts) => ts ? new Date(ts).toTimeString().substring(0, 8) : 'none';
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
React.createElement(Box, { borderStyle: 'single', borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
|
|
23
|
+
React.createElement(Text, null,
|
|
24
|
+
React.createElement(Text, { color: 'gray' }, 'MCP Server: '),
|
|
25
|
+
React.createElement(Text, { color: 'green' }, serverState)
|
|
26
|
+
),
|
|
27
|
+
React.createElement(Text, null,
|
|
28
|
+
React.createElement(Text, { color: 'gray' }, 'Port: '),
|
|
29
|
+
React.createElement(Text, { color: 'white' }, 'stdin/stdout')
|
|
30
|
+
),
|
|
31
|
+
React.createElement(Text, null,
|
|
32
|
+
React.createElement(Text, { color: 'gray' }, 'Tool calls: '),
|
|
33
|
+
React.createElement(Text, { color: 'white' }, '' + toolCount)
|
|
34
|
+
),
|
|
35
|
+
React.createElement(Text, null,
|
|
36
|
+
React.createElement(Text, { color: 'gray' }, 'Last call: '),
|
|
37
|
+
React.createElement(Text, { color: 'white' }, formatTime(lastCall))
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = McpStatusBar;
|