flowmind 1.4.0 → 1.4.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 +39 -7
- package/core/honor-engine.js +5 -5
- package/core/index.js +7 -0
- package/core/learning-engine.js +2 -4
- package/core/scene-matcher.js +2 -4
- package/core/skill-loader.js +7 -0
- package/core/utils.js +18 -0
- package/package.json +1 -1
- package/tui/app.jsx +9 -2
- package/tui/components/ChatPanel.jsx +35 -2
- package/tui/components/Sidebar.jsx +2 -1
package/bin/flowmind.js
CHANGED
|
@@ -1351,6 +1351,7 @@ program
|
|
|
1351
1351
|
.description('Launch enhanced TUI with split panels, skill browser, and dragon display')
|
|
1352
1352
|
.action(async () => {
|
|
1353
1353
|
let stdinWrapper = null;
|
|
1354
|
+
let stdinForwarder = null;
|
|
1354
1355
|
try {
|
|
1355
1356
|
// Register .jsx extension for CJS
|
|
1356
1357
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
@@ -1378,11 +1379,18 @@ program
|
|
|
1378
1379
|
}
|
|
1379
1380
|
return stdinWrapper;
|
|
1380
1381
|
};
|
|
1381
|
-
// Forward real stdin data to the wrapper
|
|
1382
|
+
// Forward real stdin data to the wrapper (store reference for cleanup)
|
|
1382
1383
|
if (realStdin.readable) {
|
|
1383
|
-
|
|
1384
|
-
if (!stdinWrapper.destroyed)
|
|
1385
|
-
|
|
1384
|
+
stdinForwarder = (chunk) => {
|
|
1385
|
+
if (!stdinWrapper.destroyed) {
|
|
1386
|
+
try {
|
|
1387
|
+
stdinWrapper.write(chunk);
|
|
1388
|
+
} catch (e) {
|
|
1389
|
+
// Ignore write-after-destroy errors
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
realStdin.on('data', stdinForwarder);
|
|
1386
1394
|
}
|
|
1387
1395
|
|
|
1388
1396
|
const { unmount, waitUntilExit } = render(
|
|
@@ -1397,6 +1405,16 @@ program
|
|
|
1397
1405
|
console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
|
|
1398
1406
|
}
|
|
1399
1407
|
} finally {
|
|
1408
|
+
// Clean up stdin listener to prevent leak
|
|
1409
|
+
if (stdinForwarder) {
|
|
1410
|
+
process.stdin.removeListener('data', stdinForwarder);
|
|
1411
|
+
}
|
|
1412
|
+
// Restore stdin to normal mode
|
|
1413
|
+
try {
|
|
1414
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1415
|
+
process.stdin.setRawMode(false);
|
|
1416
|
+
}
|
|
1417
|
+
} catch (e) { /* ignore */ }
|
|
1400
1418
|
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1401
1419
|
stdinWrapper.destroy();
|
|
1402
1420
|
}
|
|
@@ -1409,6 +1427,7 @@ program
|
|
|
1409
1427
|
.description('Launch real-time monitoring dashboard for MCP activity and events')
|
|
1410
1428
|
.action(async () => {
|
|
1411
1429
|
let stdinWrapper = null;
|
|
1430
|
+
let stdinForwarder = null;
|
|
1412
1431
|
try {
|
|
1413
1432
|
// Register .jsx extension for CJS
|
|
1414
1433
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
@@ -1437,9 +1456,14 @@ program
|
|
|
1437
1456
|
return stdinWrapper;
|
|
1438
1457
|
};
|
|
1439
1458
|
if (realStdin.readable) {
|
|
1440
|
-
|
|
1441
|
-
if (!stdinWrapper.destroyed)
|
|
1442
|
-
|
|
1459
|
+
stdinForwarder = (chunk) => {
|
|
1460
|
+
if (!stdinWrapper.destroyed) {
|
|
1461
|
+
try {
|
|
1462
|
+
stdinWrapper.write(chunk);
|
|
1463
|
+
} catch (e) { /* ignore write-after-destroy */ }
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
realStdin.on('data', stdinForwarder);
|
|
1443
1467
|
}
|
|
1444
1468
|
|
|
1445
1469
|
const { unmount, waitUntilExit } = render(
|
|
@@ -1454,6 +1478,14 @@ program
|
|
|
1454
1478
|
console.log(chalk.yellow('Try running: npm install ink@3 react'));
|
|
1455
1479
|
}
|
|
1456
1480
|
} finally {
|
|
1481
|
+
if (stdinForwarder) {
|
|
1482
|
+
process.stdin.removeListener('data', stdinForwarder);
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1486
|
+
process.stdin.setRawMode(false);
|
|
1487
|
+
}
|
|
1488
|
+
} catch (e) { /* ignore */ }
|
|
1457
1489
|
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1458
1490
|
stdinWrapper.destroy();
|
|
1459
1491
|
}
|
package/core/honor-engine.js
CHANGED
|
@@ -50,7 +50,7 @@ class HonorEngine {
|
|
|
50
50
|
}
|
|
51
51
|
this.initialized = true;
|
|
52
52
|
} catch (error) {
|
|
53
|
-
|
|
53
|
+
console.warn('HonorEngine init failed, using defaults:', error.message);
|
|
54
54
|
this.data = this.createDefaultData();
|
|
55
55
|
this.initialized = true;
|
|
56
56
|
}
|
|
@@ -92,7 +92,7 @@ class HonorEngine {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
|
-
|
|
95
|
+
console.warn('HonorEngine seedKnownSkills failed:', error.message);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -148,7 +148,7 @@ class HonorEngine {
|
|
|
148
148
|
timestamp: this.data.lastUpdated
|
|
149
149
|
});
|
|
150
150
|
} catch (error) {
|
|
151
|
-
|
|
151
|
+
console.warn('HonorEngine award failed:', error.message);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -170,7 +170,7 @@ class HonorEngine {
|
|
|
170
170
|
await this.save();
|
|
171
171
|
}
|
|
172
172
|
} catch (error) {
|
|
173
|
-
|
|
173
|
+
console.warn('HonorEngine addKnownSkill failed:', error.message);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -247,7 +247,7 @@ class HonorEngine {
|
|
|
247
247
|
await fs.ensureDir(path.dirname(this.honorPath));
|
|
248
248
|
await fs.writeJson(this.honorPath, this.data, { spaces: 2 });
|
|
249
249
|
} catch (error) {
|
|
250
|
-
|
|
250
|
+
console.warn('HonorEngine save failed:', error.message);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
}
|
package/core/index.js
CHANGED
|
@@ -33,6 +33,13 @@ class FlowMind {
|
|
|
33
33
|
if (this.initialized) return this;
|
|
34
34
|
|
|
35
35
|
await this.config.load();
|
|
36
|
+
|
|
37
|
+
// Validate configuration
|
|
38
|
+
const validation = this.config.validate();
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
console.warn('Configuration warnings:', validation.errors.join(', '));
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
await this.components.init();
|
|
37
44
|
await this.components.initAll();
|
|
38
45
|
await this.honor.init();
|
package/core/learning-engine.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('fs-extra');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { v4: uuidv4 } = require('uuid');
|
|
9
9
|
const eventBus = require('./event-bus');
|
|
10
|
+
const { expandPath } = require('./utils');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Per-key write queue to prevent concurrent read-modify-write races
|
|
@@ -580,10 +581,7 @@ class LearningEngine {
|
|
|
580
581
|
* Helper methods
|
|
581
582
|
*/
|
|
582
583
|
expandPath(filePath) {
|
|
583
|
-
|
|
584
|
-
return path.join(process.env.HOME || process.env.USERPROFILE, filePath.slice(1));
|
|
585
|
-
}
|
|
586
|
-
return filePath;
|
|
584
|
+
return expandPath(filePath);
|
|
587
585
|
}
|
|
588
586
|
|
|
589
587
|
extractCondition(input) {
|
package/core/scene-matcher.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { expandPath } = require('./utils');
|
|
8
9
|
|
|
9
10
|
class SceneMatcher {
|
|
10
11
|
constructor(config, learning) {
|
|
@@ -316,10 +317,7 @@ class SceneMatcher {
|
|
|
316
317
|
* Helper to expand path
|
|
317
318
|
*/
|
|
318
319
|
expandPath(filePath) {
|
|
319
|
-
|
|
320
|
-
return path.join(process.env.HOME || process.env.USERPROFILE, filePath.slice(1));
|
|
321
|
-
}
|
|
322
|
-
return filePath;
|
|
320
|
+
return expandPath(filePath);
|
|
323
321
|
}
|
|
324
322
|
}
|
|
325
323
|
|
package/core/skill-loader.js
CHANGED
|
@@ -327,6 +327,13 @@ class SkillLoader {
|
|
|
327
327
|
const skill = this.skills.get(name);
|
|
328
328
|
if (!skill) return null;
|
|
329
329
|
|
|
330
|
+
// Clear require cache to actually reload the module
|
|
331
|
+
const indexPath = require('path').join(skill.path, 'index.js');
|
|
332
|
+
const resolvedPath = require.resolve(indexPath);
|
|
333
|
+
if (require.cache[resolvedPath]) {
|
|
334
|
+
delete require.cache[resolvedPath];
|
|
335
|
+
}
|
|
336
|
+
|
|
330
337
|
return await this.loadSkill(name, skill.path);
|
|
331
338
|
}
|
|
332
339
|
|
package/core/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Expand ~ in file paths to home directory
|
|
10
|
+
*/
|
|
11
|
+
function expandPath(filePath) {
|
|
12
|
+
if (filePath.startsWith('~')) {
|
|
13
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || os.homedir(), filePath.slice(1));
|
|
14
|
+
}
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { expandPath };
|
package/package.json
CHANGED
package/tui/app.jsx
CHANGED
|
@@ -8,6 +8,7 @@ const StatusBar = require('./components/StatusBar.jsx');
|
|
|
8
8
|
function App({ flowmind }) {
|
|
9
9
|
const [results, setResults] = React.useState([]);
|
|
10
10
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
11
|
+
const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
|
|
11
12
|
const mountedRef = React.useRef(true);
|
|
12
13
|
const { exit } = useApp();
|
|
13
14
|
|
|
@@ -15,11 +16,16 @@ function App({ flowmind }) {
|
|
|
15
16
|
return () => { mountedRef.current = false; };
|
|
16
17
|
}, []);
|
|
17
18
|
|
|
19
|
+
// Ctrl+C always exits; Tab switches focus between panels
|
|
18
20
|
useInput((input, key) => {
|
|
19
21
|
if (key.ctrl && input === 'c') exit();
|
|
22
|
+
if (key.tab) {
|
|
23
|
+
setFocusPanel(prev => prev === 'chat' ? 'sidebar' : 'chat');
|
|
24
|
+
}
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
const handleCommand = React.useCallback(async (input, addResponse) => {
|
|
28
|
+
if (!mountedRef.current) return;
|
|
23
29
|
setIsProcessing(true);
|
|
24
30
|
try {
|
|
25
31
|
const result = await flowmind.process(input);
|
|
@@ -41,6 +47,7 @@ function App({ flowmind }) {
|
|
|
41
47
|
}, [flowmind]);
|
|
42
48
|
|
|
43
49
|
const handleSkillSelect = React.useCallback((skill) => {
|
|
50
|
+
if (!mountedRef.current) return;
|
|
44
51
|
try {
|
|
45
52
|
setResults(prev => [...prev, {
|
|
46
53
|
type: 'result',
|
|
@@ -55,9 +62,9 @@ function App({ flowmind }) {
|
|
|
55
62
|
return (
|
|
56
63
|
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
57
64
|
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
58
|
-
React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect }),
|
|
65
|
+
React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect, focused: focusPanel === 'sidebar' }),
|
|
59
66
|
React.createElement(Box, { flexDirection: 'column', width: '70%', flexGrow: 1 },
|
|
60
|
-
React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit }),
|
|
67
|
+
React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit, focused: focusPanel === 'chat' }),
|
|
61
68
|
React.createElement(ResultPanel, { results: results })
|
|
62
69
|
)
|
|
63
70
|
),
|
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
|
-
const { Box, Text } = require('ink');
|
|
2
|
+
const { Box, Text, useInput } = require('ink');
|
|
3
3
|
const TextInput = require('ink-text-input').default || require('ink-text-input');
|
|
4
4
|
const Spinner = require('ink-spinner').default || require('ink-spinner');
|
|
5
5
|
|
|
6
|
-
function ChatPanel({ onSubmit, isProcessing, onExit }) {
|
|
6
|
+
function ChatPanel({ onSubmit, isProcessing, onExit, focused }) {
|
|
7
7
|
const [input, setInput] = React.useState('');
|
|
8
8
|
const [history, setHistory] = React.useState([]);
|
|
9
|
+
const [cmdHistory, setCmdHistory] = React.useState([]);
|
|
10
|
+
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
11
|
+
const [savedInput, setSavedInput] = React.useState('');
|
|
9
12
|
const mountedRef = React.useRef(true);
|
|
10
13
|
|
|
11
14
|
React.useEffect(() => {
|
|
12
15
|
return () => { mountedRef.current = false; };
|
|
13
16
|
}, []);
|
|
14
17
|
|
|
18
|
+
// Handle Up/Down arrow for command history (only when focused)
|
|
19
|
+
useInput((ch, key) => {
|
|
20
|
+
if (!focused || isProcessing) return;
|
|
21
|
+
|
|
22
|
+
if (key.upArrow && cmdHistory.length > 0) {
|
|
23
|
+
const newIndex = historyIndex === -1
|
|
24
|
+
? cmdHistory.length - 1
|
|
25
|
+
: Math.max(0, historyIndex - 1);
|
|
26
|
+
if (historyIndex === -1) setSavedInput(input);
|
|
27
|
+
setHistoryIndex(newIndex);
|
|
28
|
+
setInput(cmdHistory[newIndex]);
|
|
29
|
+
} else if (key.downArrow) {
|
|
30
|
+
if (historyIndex === -1) return;
|
|
31
|
+
const newIndex = historyIndex + 1;
|
|
32
|
+
if (newIndex >= cmdHistory.length) {
|
|
33
|
+
setHistoryIndex(-1);
|
|
34
|
+
setInput(savedInput);
|
|
35
|
+
} else {
|
|
36
|
+
setHistoryIndex(newIndex);
|
|
37
|
+
setInput(cmdHistory[newIndex]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
15
42
|
const handleSubmit = (value) => {
|
|
16
43
|
if (!value.trim()) return;
|
|
17
44
|
setHistory(prev => [...prev, { role: 'user', text: value }]);
|
|
45
|
+
// Add to command history (deduplicate consecutive)
|
|
46
|
+
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== value) {
|
|
47
|
+
setCmdHistory(prev => [...prev, value]);
|
|
48
|
+
}
|
|
49
|
+
setHistoryIndex(-1);
|
|
50
|
+
setSavedInput('');
|
|
18
51
|
setInput('');
|
|
19
52
|
if (value.toLowerCase() === 'exit' || value.toLowerCase() === 'quit') {
|
|
20
53
|
if (onExit) onExit();
|
|
@@ -2,7 +2,7 @@ const React = require('react');
|
|
|
2
2
|
const { Box, Text, useInput } = require('ink');
|
|
3
3
|
const DragonTotem = require('./DragonTotem.jsx');
|
|
4
4
|
|
|
5
|
-
function Sidebar({ flowmind, width, onSkillSelect }) {
|
|
5
|
+
function Sidebar({ flowmind, width, onSkillSelect, focused }) {
|
|
6
6
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
7
7
|
const [skills, setSkills] = React.useState([]);
|
|
8
8
|
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
@@ -24,6 +24,7 @@ function Sidebar({ flowmind, width, onSkillSelect }) {
|
|
|
24
24
|
}, [flowmind]);
|
|
25
25
|
|
|
26
26
|
useInput((input, key) => {
|
|
27
|
+
if (!focused) return; // Ignore input when sidebar is not focused
|
|
27
28
|
if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
28
29
|
else if (key.downArrow) setSelectedIndex(prev => Math.min(skills.length - 1, prev + 1));
|
|
29
30
|
else if (key.return && skills[selectedIndex] && onSkillSelect) onSkillSelect(skills[selectedIndex]);
|