flowmind 1.4.1 → 1.4.3

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
@@ -15,23 +15,43 @@ const { execSync } = require('child_process');
15
15
  const FlowMind = require('../core');
16
16
  const HonorEngine = require('../core/honor-engine');
17
17
 
18
+ /**
19
+ * Restore terminal to sane state (cancel raw mode, show cursor, reset colors)
20
+ */
21
+ function restoreTerminal() {
22
+ try {
23
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
24
+ process.stdin.setRawMode(false);
25
+ }
26
+ } catch (e) { /* ignore */ }
27
+ // Show cursor, reset colors, clear scroll region
28
+ process.stdout.write('\x1b[?25h\x1b[0m\x1b[r');
29
+ }
30
+
18
31
  // Global error handlers to prevent silent CLI crashes
19
32
  process.on('uncaughtException', (err) => {
20
33
  console.error(chalk.red('\nUncaught Exception:'), err.message);
21
- if (process.stdin.isTTY && process.stdin.isRaw) {
22
- process.stdin.setRawMode(false);
23
- }
34
+ restoreTerminal();
24
35
  process.exit(1);
25
36
  });
26
37
 
27
38
  process.on('unhandledRejection', (reason) => {
28
39
  console.error(chalk.red('\nUnhandled Rejection:'), reason?.message || reason);
29
- if (process.stdin.isTTY && process.stdin.isRaw) {
30
- process.stdin.setRawMode(false);
31
- }
40
+ restoreTerminal();
32
41
  process.exit(1);
33
42
  });
34
43
 
44
+ // Handle SIGINT (Ctrl+C) to restore terminal state
45
+ process.on('SIGINT', () => {
46
+ restoreTerminal();
47
+ process.exit(0);
48
+ });
49
+
50
+ process.on('SIGTERM', () => {
51
+ restoreTerminal();
52
+ process.exit(0);
53
+ });
54
+
35
55
  // Package info
36
56
  const packageJson = require('../package.json');
37
57
 
@@ -506,6 +526,8 @@ program
506
526
  }
507
527
  } catch (error) {
508
528
  console.error(chalk.red('Error:'), error.message);
529
+ } finally {
530
+ restoreTerminal();
509
531
  }
510
532
  });
511
533
 
@@ -889,35 +911,47 @@ async function runInteractiveMode(fm) {
889
911
  showBanner();
890
912
  console.log(chalk.cyan('Interactive mode started. Type "exit" to quit.\n'));
891
913
 
892
- while (true) {
893
- const { input } = await inquirer.prompt([
894
- {
895
- type: 'input',
896
- name: 'input',
897
- message: chalk.green('You:'),
898
- prefix: ''
914
+ try {
915
+ while (true) {
916
+ let input;
917
+ try {
918
+ const answers = await inquirer.prompt([
919
+ {
920
+ type: 'input',
921
+ name: 'input',
922
+ message: chalk.green('You:'),
923
+ prefix: ''
924
+ }
925
+ ]);
926
+ input = answers.input;
927
+ } catch (promptErr) {
928
+ // inquirer throws on SIGINT (Ctrl+C)
929
+ console.log(chalk.cyan('\nGoodbye! 👋\n'));
930
+ break;
899
931
  }
900
- ]);
901
932
 
902
- if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
903
- console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
904
- break;
905
- }
933
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
934
+ console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
935
+ break;
936
+ }
906
937
 
907
- if (!input.trim()) continue;
938
+ if (!input.trim()) continue;
908
939
 
909
- const spinner = ora('Thinking...').start();
940
+ const spinner = ora('Thinking...').start();
910
941
 
911
- try {
912
- const result = await fm.process(input);
913
- spinner.stop();
914
- displayResult(result);
915
- } catch (error) {
916
- spinner.stop();
917
- console.error(chalk.red('Error:'), error.message);
918
- }
942
+ try {
943
+ const result = await fm.process(input);
944
+ spinner.stop();
945
+ displayResult(result);
946
+ } catch (error) {
947
+ spinner.stop();
948
+ console.error(chalk.red('Error:'), error.message);
949
+ }
919
950
 
920
- console.log(''); // Empty line for spacing
951
+ console.log(''); // Empty line for spacing
952
+ }
953
+ } finally {
954
+ restoreTerminal();
921
955
  }
922
956
  }
923
957
 
@@ -1351,6 +1385,7 @@ program
1351
1385
  .description('Launch enhanced TUI with split panels, skill browser, and dragon display')
1352
1386
  .action(async () => {
1353
1387
  let stdinWrapper = null;
1388
+ let stdinForwarder = null;
1354
1389
  try {
1355
1390
  // Register .jsx extension for CJS
1356
1391
  require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
@@ -1378,11 +1413,18 @@ program
1378
1413
  }
1379
1414
  return stdinWrapper;
1380
1415
  };
1381
- // Forward real stdin data to the wrapper
1416
+ // Forward real stdin data to the wrapper (store reference for cleanup)
1382
1417
  if (realStdin.readable) {
1383
- realStdin.on('data', (chunk) => {
1384
- if (!stdinWrapper.destroyed) stdinWrapper.write(chunk);
1385
- });
1418
+ stdinForwarder = (chunk) => {
1419
+ if (!stdinWrapper.destroyed) {
1420
+ try {
1421
+ stdinWrapper.write(chunk);
1422
+ } catch (e) {
1423
+ // Ignore write-after-destroy errors
1424
+ }
1425
+ }
1426
+ };
1427
+ realStdin.on('data', stdinForwarder);
1386
1428
  }
1387
1429
 
1388
1430
  const { unmount, waitUntilExit } = render(
@@ -1397,9 +1439,14 @@ program
1397
1439
  console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
1398
1440
  }
1399
1441
  } finally {
1442
+ // Clean up stdin listener to prevent leak
1443
+ if (stdinForwarder) {
1444
+ process.stdin.removeListener('data', stdinForwarder);
1445
+ }
1400
1446
  if (stdinWrapper && !stdinWrapper.destroyed) {
1401
1447
  stdinWrapper.destroy();
1402
1448
  }
1449
+ restoreTerminal();
1403
1450
  }
1404
1451
  });
1405
1452
 
@@ -1409,6 +1456,7 @@ program
1409
1456
  .description('Launch real-time monitoring dashboard for MCP activity and events')
1410
1457
  .action(async () => {
1411
1458
  let stdinWrapper = null;
1459
+ let stdinForwarder = null;
1412
1460
  try {
1413
1461
  // Register .jsx extension for CJS
1414
1462
  require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
@@ -1437,9 +1485,14 @@ program
1437
1485
  return stdinWrapper;
1438
1486
  };
1439
1487
  if (realStdin.readable) {
1440
- realStdin.on('data', (chunk) => {
1441
- if (!stdinWrapper.destroyed) stdinWrapper.write(chunk);
1442
- });
1488
+ stdinForwarder = (chunk) => {
1489
+ if (!stdinWrapper.destroyed) {
1490
+ try {
1491
+ stdinWrapper.write(chunk);
1492
+ } catch (e) { /* ignore write-after-destroy */ }
1493
+ }
1494
+ };
1495
+ realStdin.on('data', stdinForwarder);
1443
1496
  }
1444
1497
 
1445
1498
  const { unmount, waitUntilExit } = render(
@@ -1454,9 +1507,13 @@ program
1454
1507
  console.log(chalk.yellow('Try running: npm install ink@3 react'));
1455
1508
  }
1456
1509
  } finally {
1510
+ if (stdinForwarder) {
1511
+ process.stdin.removeListener('data', stdinForwarder);
1512
+ }
1457
1513
  if (stdinWrapper && !stdinWrapper.destroyed) {
1458
1514
  stdinWrapper.destroy();
1459
1515
  }
1516
+ restoreTerminal();
1460
1517
  }
1461
1518
  });
1462
1519
 
@@ -1592,6 +1649,8 @@ if (!process.argv.slice(2).length) {
1592
1649
  await runInteractiveMode(fm);
1593
1650
  } catch (error) {
1594
1651
  console.error(chalk.red('Error:'), error.message);
1652
+ } finally {
1653
+ restoreTerminal();
1595
1654
  }
1596
1655
  })();
1597
1656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "The AI Agent That Learns How You Work - Stop repeating yourself, FlowMind learns your workflows and applies them automatically.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
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
  ),
@@ -3,7 +3,7 @@ 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
9
  const [cmdHistory, setCmdHistory] = React.useState([]);
@@ -15,9 +15,9 @@ function ChatPanel({ onSubmit, isProcessing, onExit }) {
15
15
  return () => { mountedRef.current = false; };
16
16
  }, []);
17
17
 
18
- // Handle Up/Down arrow for command history
18
+ // Handle Up/Down arrow for command history (only when focused)
19
19
  useInput((ch, key) => {
20
- if (isProcessing) return;
20
+ if (!focused || isProcessing) return;
21
21
 
22
22
  if (key.upArrow && cmdHistory.length > 0) {
23
23
  const newIndex = historyIndex === -1
@@ -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]);