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 CHANGED
@@ -11,7 +11,26 @@ const ora = require('ora');
11
11
  const inquirer = require('inquirer');
12
12
  const fs = require('fs-extra');
13
13
  const path = require('path');
14
+ const { execSync } = require('child_process');
14
15
  const FlowMind = require('../core');
16
+ const HonorEngine = require('../core/honor-engine');
17
+
18
+ // Global error handlers to prevent silent CLI crashes
19
+ process.on('uncaughtException', (err) => {
20
+ console.error(chalk.red('\nUncaught Exception:'), err.message);
21
+ if (process.stdin.isTTY && process.stdin.isRaw) {
22
+ process.stdin.setRawMode(false);
23
+ }
24
+ process.exit(1);
25
+ });
26
+
27
+ process.on('unhandledRejection', (reason) => {
28
+ console.error(chalk.red('\nUnhandled Rejection:'), reason?.message || reason);
29
+ if (process.stdin.isTTY && process.stdin.isRaw) {
30
+ process.stdin.setRawMode(false);
31
+ }
32
+ process.exit(1);
33
+ });
15
34
 
16
35
  // Package info
17
36
  const packageJson = require('../package.json');
@@ -42,6 +61,274 @@ function showBanner() {
42
61
  `));
43
62
  }
44
63
 
64
+ /**
65
+ * Dragon Totem ASCII Art by level - Chinese Dragon (中国龙)
66
+ */
67
+ function getDragonArt(level) {
68
+ const arts = {
69
+ 0: [
70
+ ' ╭─────╮ ',
71
+ ' ╱ ╭─╮ ╲ ',
72
+ ' │ │ │ │ ',
73
+ ' │ │ ◎ │ │ ',
74
+ ' │ ╰─╯ │ ',
75
+ ' ╲ ╱ ',
76
+ ' ╰─────╯ ',
77
+ ' 龙 蛋 (Egg) '
78
+ ],
79
+ 1: [
80
+ ' ╭──╮ ',
81
+ ' ╭────╯ ╰───╮ ',
82
+ ' ╱ ◎ ╰─╯ ╲ ',
83
+ ' ╱ ▽ ╲ ',
84
+ ' ╲ ╱╲ ╱╲ ╱ ',
85
+ ' ╲╱╱ ╲╱╱ ╲╱╲╱ ',
86
+ ' 幼 龙 (Hatchling) '
87
+ ],
88
+ 2: [
89
+ ' ╭─╮ ╭─╮ ',
90
+ ' ╭────╯ ╰──╯ ╰───╮ ',
91
+ ' ╱ ◎ ╰──╯ ╲ ',
92
+ ' ╱ ╭────────╮ ╲ ',
93
+ ' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',
94
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',
95
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
96
+ ' ╰─╯ ╰─╯ ',
97
+ ' 少 年 龙 (Juvenile) '
98
+ ],
99
+ 3: [
100
+ ' ╭───╮ ╭───╮ ',
101
+ ' ╭───╯ ╰──╯ ╰───╮ ',
102
+ ' ╱ ◎ ╰───╯ ╲ ',
103
+ '│ ╭──────────╮ │ ',
104
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
105
+ ' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
106
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
107
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
108
+ ' ╰───╯ ╰───╯ ',
109
+ ' 成 年 龙 (Adult) '
110
+ ],
111
+ 4: [
112
+ ' ╭───╮ ╭───╮ ',
113
+ '╭───╯ ╰──────╯ ╰───╮ ',
114
+ '│ ◎ ╰───╯ │ ',
115
+ '│ ╭────────────╮ │ ',
116
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
117
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',
118
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',
119
+ ' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',
120
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
121
+ ' ╰───╯ ╰───╯ ',
122
+ ' 长 老 龙 (Elder) '
123
+ ],
124
+ 5: [
125
+ ' ★ ╭───╮ ╭───╮ ★ ',
126
+ '╭─╯ ╰──╯ ╰──╯ ╰─╮ ',
127
+ '│ ◎ ╰───╯ │ ',
128
+ '│ ╭──────────────╮ │ ',
129
+ '│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',
130
+ ' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
131
+ ' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',
132
+ ' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',
133
+ ' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',
134
+ ' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',
135
+ ' ╰───╯ ╰───╯ ',
136
+ ' 升 龙 (Ascended) '
137
+ ]
138
+ };
139
+ return arts[level] || arts[0];
140
+ }
141
+
142
+ /**
143
+ * Get dragon color function for a level
144
+ */
145
+ function getDragonColor(level) {
146
+ const colors = {
147
+ 0: (text) => chalk.gray(text),
148
+ 1: (text) => chalk.cyan(text),
149
+ 2: (text) => chalk.cyan.bold(text),
150
+ 3: (text) => chalk.cyanBright(text),
151
+ 4: (text) => chalk.cyanBright.bold(text),
152
+ 5: (text) => chalk.cyanBright.bold(text)
153
+ };
154
+ return colors[level] || colors[0];
155
+ }
156
+
157
+ /**
158
+ * Get highlight color for Ascended level
159
+ */
160
+ function getHighlightColor(level) {
161
+ if (level >= 5) {
162
+ return (text) => chalk.whiteBright(text);
163
+ }
164
+ return (text) => text;
165
+ }
166
+
167
+ /**
168
+ * Render dragon totem with honor data
169
+ */
170
+ function renderDragonTotem(honorData) {
171
+ const info = getLevelInfo(honorData.points);
172
+ const dragonColor = getDragonColor(info.level);
173
+ const highlightColor = getHighlightColor(info.level);
174
+ const art = getDragonArt(info.level);
175
+
176
+ // Calculate max art line width for proper padding
177
+ const maxArtWidth = Math.max(...art.map(l => l.length));
178
+ const boxWidth = Math.max(maxArtWidth + 8, 55);
179
+
180
+ const border = '─'.repeat(boxWidth - 2);
181
+ const padRight = (text, width) => {
182
+ const padding = Math.max(0, width - text.length);
183
+ return ' '.repeat(padding);
184
+ };
185
+
186
+ console.log('');
187
+ console.log(chalk.cyan(' ┌' + border + '┐'));
188
+ console.log(chalk.cyan(' │') + ' 🐉 Dragon Totem of Honor ' + padRight(' 🐉 Dragon Totem of Honor ', boxWidth - 2) + chalk.cyan('│'));
189
+ console.log(chalk.cyan(' ├' + border + '┤'));
190
+
191
+ for (const line of art) {
192
+ const padded = ' ' + line + padRight(' ' + line, boxWidth - 2);
193
+ console.log(chalk.cyan(' │') + dragonColor(padded) + chalk.cyan('│'));
194
+ }
195
+
196
+ console.log(chalk.cyan(' ├' + border + '┤'));
197
+
198
+ const levelLine = ` Level: ${info.level} - ${info.name}`;
199
+ const pointsLine = ` Points: ${honorData.points}`;
200
+ const stateLine = ` State: ${info.state}`;
201
+
202
+ console.log(chalk.cyan(' │') + highlightColor(levelLine + padRight(levelLine, boxWidth - 2)) + chalk.cyan('│'));
203
+ console.log(chalk.cyan(' │') + highlightColor(pointsLine + padRight(pointsLine, boxWidth - 2)) + chalk.cyan('│'));
204
+ console.log(chalk.cyan(' │') + highlightColor(stateLine + padRight(stateLine, boxWidth - 2)) + chalk.cyan('│'));
205
+
206
+ if (info.nextLevel) {
207
+ const nextLine = ` Next: ${info.pointsToNext} points to ${info.nextLevelName}`;
208
+ console.log(chalk.cyan(' │') + chalk.gray(nextLine + padRight(nextLine, boxWidth - 2)) + chalk.cyan('│'));
209
+ } else {
210
+ const maxLine = ' Maximum level reached!';
211
+ console.log(chalk.cyan(' │') + chalk.yellow(maxLine + padRight(maxLine, boxWidth - 2)) + chalk.cyan('│'));
212
+ }
213
+
214
+ console.log(chalk.cyan(' ├' + border + '┤'));
215
+
216
+ const statsTitle = ' Stats:';
217
+ console.log(chalk.cyan(' │') + chalk.white(statsTitle + padRight(statsTitle, boxWidth - 2)) + chalk.cyan('│'));
218
+
219
+ const stats1 = ` Skills Used: ${honorData.stats.skillUseCount}`;
220
+ const stats2 = ` New Skills: ${honorData.stats.newSkillCount}`;
221
+ const stats3 = ` Learnings: ${honorData.stats.learningCount}`;
222
+
223
+ console.log(chalk.cyan(' │') + chalk.gray(stats1 + padRight(stats1, boxWidth - 2)) + chalk.cyan('│'));
224
+ console.log(chalk.cyan(' │') + chalk.gray(stats2 + padRight(stats2, boxWidth - 2)) + chalk.cyan('│'));
225
+ console.log(chalk.cyan(' │') + chalk.gray(stats3 + padRight(stats3, boxWidth - 2)) + chalk.cyan('│'));
226
+
227
+ console.log(chalk.cyan(' └' + border + '┘'));
228
+ console.log('');
229
+ }
230
+
231
+ /**
232
+ * Get level info for points
233
+ */
234
+ function getLevelInfo(points) {
235
+ const levels = [
236
+ { level: 0, minPoints: 0, name: 'Egg', state: 'dormant' },
237
+ { level: 1, minPoints: 1, name: 'Hatchling', state: 'awakening' },
238
+ { level: 2, minPoints: 10, name: 'Juvenile', state: 'growing' },
239
+ { level: 3, minPoints: 30, name: 'Adult', state: 'soaring' },
240
+ { level: 4, minPoints: 60, name: 'Elder', state: 'wise' },
241
+ { level: 5, minPoints: 100, name: 'Ascended', state: 'transcendent' }
242
+ ];
243
+
244
+ let current = levels[0];
245
+ let next = levels[1];
246
+
247
+ for (const tier of levels) {
248
+ if (points >= tier.minPoints) {
249
+ current = tier;
250
+ next = levels[tier.level + 1] || null;
251
+ }
252
+ }
253
+
254
+ return {
255
+ level: current.level,
256
+ name: current.name,
257
+ state: current.state,
258
+ nextLevel: next ? next.level : null,
259
+ nextLevelName: next ? next.name : null,
260
+ pointsToNext: next ? next.minPoints - points : 0
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Get hint for next level
266
+ */
267
+ function getNextLevelHint(points) {
268
+ const info = getLevelInfo(points);
269
+ if (info.nextLevel) {
270
+ return `Earn ${info.pointsToNext} more points to reach ${info.nextLevelName} (Level ${info.nextLevel})`;
271
+ }
272
+ return 'You have reached the maximum dragon level!';
273
+ }
274
+
275
+ /**
276
+ * Publish honor data
277
+ */
278
+ async function publishHonor(options) {
279
+ const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.flowmind');
280
+ const honorPath = path.join(configDir, 'honor.json');
281
+
282
+ if (!await fs.pathExists(honorPath)) {
283
+ console.error(chalk.red('No honor data found. Run flowmind init first.'));
284
+ return;
285
+ }
286
+
287
+ const honorData = await fs.readJson(honorPath);
288
+ const info = getLevelInfo(honorData.points);
289
+
290
+ const exportData = {
291
+ version: honorData.version,
292
+ points: honorData.points,
293
+ level: info.level,
294
+ levelName: info.name,
295
+ state: info.state,
296
+ stats: honorData.stats,
297
+ knownSkillsCount: honorData.knownSkills ? honorData.knownSkills.length : 0,
298
+ recentHistory: (honorData.history || []).slice(-20),
299
+ exportedAt: new Date().toISOString()
300
+ };
301
+
302
+ // Export to local JSON file
303
+ const outputPath = options.output || 'flowmind-honor.json';
304
+ await fs.writeJson(outputPath, exportData, { spaces: 2 });
305
+ console.log(chalk.green(`✓ Honor data exported to: ${outputPath}`));
306
+
307
+ // Create GitHub Gist if requested
308
+ if (options.gist) {
309
+ try {
310
+ // Check if gh CLI is available
311
+ try {
312
+ execSync('gh --version', { stdio: 'ignore' });
313
+ } catch {
314
+ console.error(chalk.red('GitHub CLI (gh) is not installed. Install it from https://cli.github.com/'));
315
+ return;
316
+ }
317
+
318
+ const gistContent = JSON.stringify(exportData, null, 2);
319
+ const gistDescription = `FlowMind Honor: Level ${info.level} (${info.name}) - ${honorData.points} points`;
320
+
321
+ // Create gist using gh CLI
322
+ const gistCommand = `gh gist create --public --desc "${gistDescription}" --filename "flowmind-honor.json" - <<< '${gistContent.replace(/'/g, "'\\''")}'`;
323
+
324
+ const gistUrl = execSync(gistCommand, { encoding: 'utf-8' }).trim();
325
+ console.log(chalk.green(`✓ GitHub Gist created: ${gistUrl}`));
326
+ } catch (error) {
327
+ console.error(chalk.red(`Failed to create Gist: ${error.message}`));
328
+ }
329
+ }
330
+ }
331
+
45
332
  // CLI Commands
46
333
  program
47
334
  .name('flowmind')
@@ -173,6 +460,17 @@ program
173
460
  console.log(' Or configure manually:', chalk.yellow('~/.flowmind/ai-config.json'));
174
461
  }
175
462
 
463
+ // Award honor point for init
464
+ try {
465
+ const honorEngine = new HonorEngine({ get: (key, def) => def });
466
+ honorEngine.honorPath = path.join(configDir, 'honor.json');
467
+ await honorEngine.init();
468
+ await honorEngine.award('init', 'FlowMind initialized');
469
+ console.log(chalk.green('\n✓ Honor system activated (+1 point)'));
470
+ } catch (honorError) {
471
+ // Non-blocking
472
+ }
473
+
176
474
  } catch (error) {
177
475
  spinner.fail('Failed to initialize FlowMind');
178
476
  console.error(chalk.red(error.message));
@@ -446,6 +744,32 @@ program
446
744
  }
447
745
  });
448
746
 
747
+ // Honor command
748
+ program
749
+ .command('honor')
750
+ .description('Show honor points and dragon totem')
751
+ .option('-j, --json', 'Output as JSON')
752
+ .option('-p, --publish', 'Export honor data to JSON file')
753
+ .option('-g, --gist', 'Create GitHub Gist (with --publish)')
754
+ .option('-o, --output <file>', 'Output file path (default: flowmind-honor.json)')
755
+ .action(async (options) => {
756
+ try {
757
+ const fm = await initFlowMind();
758
+ const honorData = fm.getHonorData();
759
+
760
+ if (options.json) {
761
+ console.log(JSON.stringify(honorData, null, 2));
762
+ } else if (options.publish) {
763
+ await publishHonor({ output: options.output, gist: options.gist });
764
+ } else {
765
+ renderDragonTotem(honorData);
766
+ console.log(chalk.gray(` ${getNextLevelHint(honorData.points)}`));
767
+ }
768
+ } catch (error) {
769
+ console.error(chalk.red('Error:'), error.message);
770
+ }
771
+ });
772
+
449
773
  // Config command
450
774
  program
451
775
  .command('config')
@@ -1014,6 +1338,136 @@ async function openInEditor(filePath) {
1014
1338
  });
1015
1339
  }
1016
1340
 
1341
+ // TUI command - Rich terminal interface using Ink
1342
+ program
1343
+ .command('tui')
1344
+ .description('Launch enhanced TUI with split panels, skill browser, and dragon display')
1345
+ .action(async () => {
1346
+ try {
1347
+ // Register .jsx extension for CJS
1348
+ require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
1349
+
1350
+ const React = require('react');
1351
+ const { render } = require('ink');
1352
+ const App = require('../tui/app.jsx');
1353
+
1354
+ const fm = await initFlowMind();
1355
+
1356
+ const { unmount, waitUntilExit } = render(React.createElement(App, { flowmind: fm }));
1357
+ await waitUntilExit();
1358
+ unmount();
1359
+ } catch (error) {
1360
+ console.error(chalk.red('TUI Error:'), error.message);
1361
+ if (error.message.includes('Cannot find module')) {
1362
+ console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
1363
+ }
1364
+ }
1365
+ });
1366
+
1367
+ // Dashboard command - Hybrid monitoring dashboard
1368
+ program
1369
+ .command('dashboard')
1370
+ .description('Launch real-time monitoring dashboard for MCP activity and events')
1371
+ .action(async () => {
1372
+ try {
1373
+ // Register .jsx extension for CJS
1374
+ require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
1375
+
1376
+ const React = require('react');
1377
+ const { render } = require('ink');
1378
+ const DashboardApp = require('../dashboard/app.jsx');
1379
+ const eventBus = require('../core/event-bus');
1380
+
1381
+ const fm = await initFlowMind();
1382
+
1383
+ const { unmount, waitUntilExit } = render(
1384
+ React.createElement(DashboardApp, { flowmind: fm, eventBus })
1385
+ );
1386
+ await waitUntilExit();
1387
+ unmount();
1388
+ } catch (error) {
1389
+ console.error(chalk.red('Dashboard Error:'), error.message);
1390
+ if (error.message.includes('Cannot find module')) {
1391
+ console.log(chalk.yellow('Try running: npm install ink@3 react'));
1392
+ }
1393
+ }
1394
+ });
1395
+
1396
+ // Update command - Auto-update flowmind
1397
+ program
1398
+ .command('update')
1399
+ .description('Update FlowMind to the latest version')
1400
+ .option('--check', 'Only check for updates, do not install')
1401
+ .action(async (options) => {
1402
+ const { execSync } = require('child_process');
1403
+ const currentVersion = packageJson.version;
1404
+
1405
+ console.log(chalk.cyan(`\nCurrent version: ${currentVersion}`));
1406
+
1407
+ try {
1408
+ // Check latest version on npm
1409
+ const latestVersion = execSync('npm view flowmind version', { encoding: 'utf-8' }).trim();
1410
+ console.log(chalk.cyan(`Latest version: ${latestVersion}`));
1411
+
1412
+ if (currentVersion === latestVersion) {
1413
+ console.log(chalk.green('\n✓ You are already on the latest version!'));
1414
+ return;
1415
+ }
1416
+
1417
+ // Compare versions
1418
+ const parse = (v) => v.split('.').map(Number);
1419
+ const curr = parse(currentVersion);
1420
+ const latest = parse(latestVersion);
1421
+ const isNewer = latest[0] > curr[0] || (latest[0] === curr[0] && latest[1] > curr[1]) || (latest[0] === curr[0] && latest[1] === curr[1] && latest[2] > curr[2]);
1422
+
1423
+ if (!isNewer) {
1424
+ console.log(chalk.green('\n✓ You are on a newer version than npm latest.'));
1425
+ return;
1426
+ }
1427
+
1428
+ console.log(chalk.yellow(`\n⬆ Update available: ${currentVersion} → ${latestVersion}`));
1429
+
1430
+ if (options.check) {
1431
+ console.log(chalk.cyan('\nRun `flowmind update` to install.'));
1432
+ return;
1433
+ }
1434
+
1435
+ // Detect install method
1436
+ const isGlobal = (() => {
1437
+ try {
1438
+ const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
1439
+ const localPath = require.resolve('../package.json');
1440
+ return localPath.startsWith(globalRoot);
1441
+ } catch { return false; }
1442
+ })();
1443
+
1444
+ const installCmd = isGlobal
1445
+ ? `npm install -g flowmind@${latestVersion}`
1446
+ : `npm install flowmind@${latestVersion}`;
1447
+
1448
+ console.log(chalk.cyan(`\nInstalling: ${installCmd}`));
1449
+ const spinner = ora('Updating FlowMind...').start();
1450
+
1451
+ try {
1452
+ execSync(installCmd, { encoding: 'utf-8', stdio: 'pipe' });
1453
+ spinner.succeed(`FlowMind updated to ${latestVersion}!`);
1454
+
1455
+ console.log(chalk.green('\n✓ Update complete!'));
1456
+ console.log(chalk.gray(' Run `flowmind --version` to verify.'));
1457
+ } catch (installError) {
1458
+ spinner.fail('Update failed');
1459
+ console.error(chalk.red('\nInstall error:'), installError.message);
1460
+ console.log(chalk.yellow('\nTry manually:'));
1461
+ console.log(chalk.white(` ${installCmd}`));
1462
+ }
1463
+
1464
+ } catch (error) {
1465
+ console.error(chalk.red('Failed to check for updates:'), error.message);
1466
+ console.log(chalk.yellow('\nYou can manually update with:'));
1467
+ console.log(chalk.white(' npm install -g flowmind@latest'));
1468
+ }
1469
+ });
1470
+
1017
1471
  // Parse arguments
1018
1472
  program.parse(process.argv);
1019
1473
 
@@ -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();