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.
@@ -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.learning = new LearningEngine(this.config);
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
- return this.formatResult(summary || result, {
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}
@@ -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);
@@ -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;