flowmind 1.4.2 → 1.4.4

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/README_CN.md CHANGED
@@ -319,6 +319,16 @@ Claude:我来使用 FlowMind 的代码审查技能...
319
319
  通过 JSON 输出与 Codex 集成。
320
320
 
321
321
  ```bash
322
+ # 推荐:使用 Codex 包装命令
323
+ flowmind-codex "查询最近1小时的错误日志"
324
+ flowmind-codex skills
325
+ flowmind-codex skill log-audit
326
+ flowmind-codex doctor
327
+
328
+ # 默认使用当前工作区的 .flowmind-codex/
329
+ # 也可以自定义目录
330
+ # FLOWMIND_CODEX_HOME=/your/path flowmind-codex skills
331
+
322
332
  # 获取技能列表 (JSON 格式)
323
333
  flowmind skills --json
324
334
 
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const FlowMind = require('../core');
7
+ const { version } = require('../package.json');
8
+
9
+ function printJson(data) {
10
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
11
+ }
12
+
13
+ function fail(message, extra = {}) {
14
+ printJson({
15
+ ok: false,
16
+ error: {
17
+ message,
18
+ ...extra
19
+ }
20
+ });
21
+ process.exitCode = 1;
22
+ }
23
+
24
+ async function createFlowMind() {
25
+ const codexHome = process.env.FLOWMIND_CODEX_HOME || path.join(process.cwd(), '.flowmind-codex');
26
+ process.env.FLOWMIND_HOME = process.env.FLOWMIND_HOME || codexHome;
27
+
28
+ const configPath = path.join(codexHome, 'config.json');
29
+ await fs.ensureDir(codexHome);
30
+
31
+ if (!await fs.pathExists(configPath)) {
32
+ await fs.writeJson(configPath, {
33
+ version: '1.0.0',
34
+ name: 'FlowMind Codex Workspace',
35
+ storagePath: codexHome,
36
+ learning: {
37
+ storagePath: path.join(codexHome, 'learning')
38
+ }
39
+ }, { spaces: 2 });
40
+ }
41
+
42
+ const flowmind = new FlowMind({ configPath });
43
+ await flowmind.init();
44
+ return flowmind;
45
+ }
46
+
47
+ program
48
+ .name('flowmind-codex')
49
+ .description('Codex-friendly JSON wrapper for FlowMind')
50
+ .version(version);
51
+
52
+ program
53
+ .argument('[input...]', 'Process a FlowMind request directly')
54
+ .option('-s, --skill <skill>', 'Use a specific skill')
55
+ .action(async (inputParts, options) => {
56
+ if (!inputParts.length) {
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const flowmind = await createFlowMind();
62
+ const input = inputParts.join(' ');
63
+ const result = await flowmind.process(input, {
64
+ skill: options.skill
65
+ });
66
+
67
+ printJson({
68
+ ok: result.type !== 'error',
69
+ command: 'ask',
70
+ input,
71
+ result
72
+ });
73
+
74
+ if (result.type === 'error') {
75
+ process.exitCode = 1;
76
+ }
77
+ } catch (error) {
78
+ fail(error.message, { command: 'ask' });
79
+ }
80
+ });
81
+
82
+ program
83
+ .command('ask')
84
+ .description('Process a request and emit JSON')
85
+ .argument('<input...>', 'Input to process')
86
+ .option('-s, --skill <skill>', 'Use a specific skill')
87
+ .action(async (inputParts, options) => {
88
+ try {
89
+ const flowmind = await createFlowMind();
90
+ const input = inputParts.join(' ');
91
+ const result = await flowmind.process(input, {
92
+ skill: options.skill
93
+ });
94
+
95
+ printJson({
96
+ ok: result.type !== 'error',
97
+ command: 'ask',
98
+ input,
99
+ result
100
+ });
101
+
102
+ if (result.type === 'error') {
103
+ process.exitCode = 1;
104
+ }
105
+ } catch (error) {
106
+ fail(error.message, { command: 'ask' });
107
+ }
108
+ });
109
+
110
+ program
111
+ .command('skills')
112
+ .description('List skills as JSON')
113
+ .action(async () => {
114
+ try {
115
+ const flowmind = await createFlowMind();
116
+ printJson({
117
+ ok: true,
118
+ command: 'skills',
119
+ skills: flowmind.skills.list()
120
+ });
121
+ } catch (error) {
122
+ fail(error.message, { command: 'skills' });
123
+ }
124
+ });
125
+
126
+ program
127
+ .command('skill')
128
+ .description('Get one skill as JSON')
129
+ .argument('<name>', 'Skill name')
130
+ .action(async (name) => {
131
+ try {
132
+ const flowmind = await createFlowMind();
133
+ const skill = flowmind.skills.get(name);
134
+
135
+ if (!skill) {
136
+ fail(`Skill not found: ${name}`, { command: 'skill', skill: name });
137
+ return;
138
+ }
139
+
140
+ printJson({
141
+ ok: true,
142
+ command: 'skill',
143
+ skill: {
144
+ name: skill.name,
145
+ description: skill.definition?.description,
146
+ category: skill.definition?.category || skill.definition?.metadata?.category,
147
+ version: skill.definition?.version || skill.definition?.metadata?.version,
148
+ author: skill.definition?.author || skill.definition?.metadata?.author,
149
+ triggers: skill.definition?.triggers || [],
150
+ componentDependencies: skill.definition?.componentDependencies || []
151
+ }
152
+ });
153
+ } catch (error) {
154
+ fail(error.message, { command: 'skill', skill: name });
155
+ }
156
+ });
157
+
158
+ program
159
+ .command('doctor')
160
+ .description('Run health checks as JSON')
161
+ .action(async () => {
162
+ try {
163
+ const flowmind = await createFlowMind();
164
+ const result = await flowmind.doctor();
165
+ printJson({
166
+ ok: result.summary?.errors === 0,
167
+ command: 'doctor',
168
+ result
169
+ });
170
+
171
+ if (result.summary?.errors > 0) {
172
+ process.exitCode = 1;
173
+ }
174
+ } catch (error) {
175
+ fail(error.message, { command: 'doctor' });
176
+ }
177
+ });
178
+
179
+ program
180
+ .command('ai-status')
181
+ .description('Show AI status as JSON')
182
+ .action(async () => {
183
+ try {
184
+ const flowmind = await createFlowMind();
185
+ printJson({
186
+ ok: true,
187
+ command: 'ai-status',
188
+ result: flowmind.getAIStatus()
189
+ });
190
+ } catch (error) {
191
+ fail(error.message, { command: 'ai-status' });
192
+ }
193
+ });
194
+
195
+ program.parseAsync(process.argv).catch((error) => {
196
+ fail(error.message);
197
+ });
package/bin/flowmind.js CHANGED
@@ -15,23 +15,45 @@ 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
+ if (process.stdout.isTTY) {
28
+ // Show cursor, reset colors, clear scroll region
29
+ process.stdout.write('\x1b[?25h\x1b[0m\x1b[r');
30
+ }
31
+ }
32
+
18
33
  // Global error handlers to prevent silent CLI crashes
19
34
  process.on('uncaughtException', (err) => {
20
35
  console.error(chalk.red('\nUncaught Exception:'), err.message);
21
- if (process.stdin.isTTY && process.stdin.isRaw) {
22
- process.stdin.setRawMode(false);
23
- }
36
+ restoreTerminal();
24
37
  process.exit(1);
25
38
  });
26
39
 
27
40
  process.on('unhandledRejection', (reason) => {
28
41
  console.error(chalk.red('\nUnhandled Rejection:'), reason?.message || reason);
29
- if (process.stdin.isTTY && process.stdin.isRaw) {
30
- process.stdin.setRawMode(false);
31
- }
42
+ restoreTerminal();
32
43
  process.exit(1);
33
44
  });
34
45
 
46
+ // Handle SIGINT (Ctrl+C) to restore terminal state
47
+ process.on('SIGINT', () => {
48
+ restoreTerminal();
49
+ process.exit(0);
50
+ });
51
+
52
+ process.on('SIGTERM', () => {
53
+ restoreTerminal();
54
+ process.exit(0);
55
+ });
56
+
35
57
  // Package info
36
58
  const packageJson = require('../package.json');
37
59
 
@@ -483,6 +505,7 @@ program
483
505
  .alias('p')
484
506
  .description('Process a request')
485
507
  .argument('[input]', 'Input to process')
508
+ .option('-j, --json', 'Output as JSON (for tool integration)')
486
509
  .option('-s, --skill <skill>', 'Use specific skill')
487
510
  .option('-v, --verbose', 'Verbose output')
488
511
  .action(async (input, options) => {
@@ -494,18 +517,38 @@ program
494
517
  await runInteractiveMode(fm);
495
518
  } else {
496
519
  // Single command mode
497
- const spinner = ora('Processing...').start();
520
+ const spinner = options.json ? null : ora('Processing...').start();
498
521
 
499
522
  const result = await fm.process(input, {
500
523
  skill: options.skill,
501
524
  verbose: options.verbose
502
525
  });
503
526
 
504
- spinner.stop();
505
- displayResult(result);
527
+ if (spinner) {
528
+ spinner.stop();
529
+ }
530
+
531
+ if (options.json) {
532
+ console.log(JSON.stringify(result, null, 2));
533
+ if (result.type === 'error') {
534
+ process.exitCode = 1;
535
+ }
536
+ } else {
537
+ displayResult(result);
538
+ }
506
539
  }
507
540
  } catch (error) {
508
- console.error(chalk.red('Error:'), error.message);
541
+ if (options.json) {
542
+ console.log(JSON.stringify({
543
+ type: 'error',
544
+ message: error.message
545
+ }, null, 2));
546
+ process.exitCode = 1;
547
+ } else {
548
+ console.error(chalk.red('Error:'), error.message);
549
+ }
550
+ } finally {
551
+ restoreTerminal();
509
552
  }
510
553
  });
511
554
 
@@ -889,35 +932,47 @@ async function runInteractiveMode(fm) {
889
932
  showBanner();
890
933
  console.log(chalk.cyan('Interactive mode started. Type "exit" to quit.\n'));
891
934
 
892
- while (true) {
893
- const { input } = await inquirer.prompt([
894
- {
895
- type: 'input',
896
- name: 'input',
897
- message: chalk.green('You:'),
898
- prefix: ''
935
+ try {
936
+ while (true) {
937
+ let input;
938
+ try {
939
+ const answers = await inquirer.prompt([
940
+ {
941
+ type: 'input',
942
+ name: 'input',
943
+ message: chalk.green('You:'),
944
+ prefix: ''
945
+ }
946
+ ]);
947
+ input = answers.input;
948
+ } catch (promptErr) {
949
+ // inquirer throws on SIGINT (Ctrl+C)
950
+ console.log(chalk.cyan('\nGoodbye! 👋\n'));
951
+ break;
899
952
  }
900
- ]);
901
953
 
902
- if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
903
- console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
904
- break;
905
- }
954
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
955
+ console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
956
+ break;
957
+ }
906
958
 
907
- if (!input.trim()) continue;
959
+ if (!input.trim()) continue;
908
960
 
909
- const spinner = ora('Thinking...').start();
961
+ const spinner = ora('Thinking...').start();
910
962
 
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
- }
963
+ try {
964
+ const result = await fm.process(input);
965
+ spinner.stop();
966
+ displayResult(result);
967
+ } catch (error) {
968
+ spinner.stop();
969
+ console.error(chalk.red('Error:'), error.message);
970
+ }
919
971
 
920
- console.log(''); // Empty line for spacing
972
+ console.log(''); // Empty line for spacing
973
+ }
974
+ } finally {
975
+ restoreTerminal();
921
976
  }
922
977
  }
923
978
 
@@ -1409,15 +1464,10 @@ program
1409
1464
  if (stdinForwarder) {
1410
1465
  process.stdin.removeListener('data', stdinForwarder);
1411
1466
  }
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 */ }
1418
1467
  if (stdinWrapper && !stdinWrapper.destroyed) {
1419
1468
  stdinWrapper.destroy();
1420
1469
  }
1470
+ restoreTerminal();
1421
1471
  }
1422
1472
  });
1423
1473
 
@@ -1481,14 +1531,10 @@ program
1481
1531
  if (stdinForwarder) {
1482
1532
  process.stdin.removeListener('data', stdinForwarder);
1483
1533
  }
1484
- try {
1485
- if (process.stdin.isTTY && process.stdin.setRawMode) {
1486
- process.stdin.setRawMode(false);
1487
- }
1488
- } catch (e) { /* ignore */ }
1489
1534
  if (stdinWrapper && !stdinWrapper.destroyed) {
1490
1535
  stdinWrapper.destroy();
1491
1536
  }
1537
+ restoreTerminal();
1492
1538
  }
1493
1539
  });
1494
1540
 
@@ -1624,6 +1670,8 @@ if (!process.argv.slice(2).length) {
1624
1670
  await runInteractiveMode(fm);
1625
1671
  } catch (error) {
1626
1672
  console.error(chalk.red('Error:'), error.message);
1673
+ } finally {
1674
+ restoreTerminal();
1627
1675
  }
1628
1676
  })();
1629
1677
  }
@@ -312,7 +312,7 @@ class ConfigManager {
312
312
  * Get home directory
313
313
  */
314
314
  getHomeDir() {
315
- return process.env.HOME || process.env.USERPROFILE || os.homedir();
315
+ return process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
316
316
  }
317
317
 
318
318
  /**
@@ -26,20 +26,26 @@ const DRAGON_LEVELS = [
26
26
  class HonorEngine {
27
27
  constructor(config) {
28
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
- );
29
+ this.honorPath = null;
33
30
  this.skillsPath = config.get('skills.path', path.join(__dirname, '..', 'skills'));
34
31
  this.data = null;
35
32
  this.initialized = false;
36
33
  }
37
34
 
35
+ resolveHonorPath() {
36
+ const storagePath = this.config.get('storagePath')
37
+ || process.env.FLOWMIND_HOME
38
+ || path.join(process.env.HOME || process.env.USERPROFILE, '.flowmind');
39
+ return path.join(storagePath, 'honor.json');
40
+ }
41
+
38
42
  /**
39
43
  * Initialize honor engine
40
44
  */
41
45
  async init() {
42
46
  try {
47
+ this.honorPath = this.resolveHonorPath();
48
+
43
49
  if (await fs.pathExists(this.honorPath)) {
44
50
  this.data = await fs.readJson(this.honorPath);
45
51
  } else {
package/core/index.js CHANGED
@@ -116,9 +116,16 @@ class FlowMind {
116
116
 
117
117
  // 4. Select skill (AI-assisted if available)
118
118
  let skill = null;
119
+ if (context.skill) {
120
+ skill = this.skills.get(context.skill);
121
+ if (!skill) {
122
+ return this.formatError(`Skill not found: ${context.skill}`, input);
123
+ }
124
+ }
125
+
119
126
  const candidates = await this.skills.getCandidates(input, context);
120
127
 
121
- if (candidates.length > 0) {
128
+ if (!skill && candidates.length > 0) {
122
129
  // Use AI to select skill if available
123
130
  const aiSelection = await this.ai.selectSkill(input, candidates);
124
131
  if (aiSelection && aiSelection.selectedSkill) {
@@ -32,7 +32,7 @@ class LearningEngine {
32
32
  constructor(config, honorEngine = null) {
33
33
  this.config = config;
34
34
  this.honorEngine = honorEngine;
35
- this.learningPath = config.get('learning.storagePath', '~/.flowmind/learning');
35
+ this.learningPath = null;
36
36
  this.writeQueue = new WriteQueue();
37
37
  this.records = {};
38
38
  this.skillBindings = {};
@@ -44,6 +44,11 @@ class LearningEngine {
44
44
  * Initialize learning engine
45
45
  */
46
46
  async init() {
47
+ this.learningPath = this.config.get(
48
+ 'learning.storagePath',
49
+ path.join(process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || '', '.flowmind', 'learning')
50
+ );
51
+
47
52
  // Ensure directories exist
48
53
  await fs.ensureDir(this.expandPath(this.learningPath));
49
54
  await fs.ensureDir(path.join(this.expandPath(this.learningPath), 'records'));
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
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": {
7
7
  "flowmind": "./bin/flowmind.js",
8
+ "flowmind-codex": "./bin/flowmind-codex.js",
8
9
  "flowmind-mcp": "./mcp/server.js"
9
10
  },
10
11
  "scripts": {