flowmind 1.3.0 → 1.4.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## [1.3.0] - 2026-06-26
4
+
5
+ ### Fixed
6
+ - Missing `os` import in config-manager.js
7
+ - MCP Server version hardcoded instead of reading from package.json
8
+ - `config --set` and `skill --set` CLI commands broken (Commander arg parsing)
9
+ - `learn --reset` and `learn --delete` were unimplemented stubs
10
+ - Learning engine race condition on concurrent file writes (added WriteQueue)
11
+ - TUI/Dashboard crash on non-TTY stdin (Ink raw mode error)
12
+
13
+ ### Added
14
+ - All 17 skills now have working `execute()` implementations
15
+ - Global uncaughtException/unhandledRejection handlers
16
+ - Graceful exit handling in TUI via onExit callback
17
+
18
+ ## [1.2.2] - 2026-06-26
19
+
20
+ ### Fixed
21
+ - ChatPanel React import issue with ink-text-input and ink-spinner ESM/CJS interop
22
+
23
+ ## [1.2.0] - 2026-06-26
24
+
25
+ ### Added
26
+ - TUI with split panels, skill browser, dragon display
27
+ - Dashboard with real-time activity feed and stats
28
+ - Event system for cross-component communication
29
+ - Update command for auto-updating flowmind
30
+
31
+ ## [1.1.0] - 2026-06-25
32
+
33
+ ### Added
34
+ - Mainstream AI model providers (GLM, MiMo, Qwen, ERNIE, DeepSeek)
35
+ - AI model integration with rule-based fallback
36
+ - MCP Server for Claude/Codex integration
37
+
38
+ ## [1.0.1] - 2026-06-24
39
+
40
+ ### Added
41
+ - Initial release
42
+ - Skill loading and execution engine
43
+ - Learning engine for user corrections and preferences
44
+ - Honor engine with dragon totem gamification
45
+ - Scene matcher for workflow automation
46
+ - Configuration manager
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
10
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
11
- [![Version](https://img.shields.io/badge/version-1.1.0-blue)](CHANGELOG.md)
11
+ [![Version](https://img.shields.io/badge/version-1.3.0-blue)](CHANGELOG.md)
12
12
 
13
13
  [中文](README_CN.md) | [Quick Start](#-quick-start) | [How It Works](#-how-it-works) | [Use Cases](#-use-cases) | [Architecture](#-architecture)
14
14
 
package/bin/flowmind.js CHANGED
@@ -1460,6 +1460,52 @@ program
1460
1460
  }
1461
1461
  });
1462
1462
 
1463
+ // Doctor command - System health check
1464
+ program
1465
+ .command('doctor')
1466
+ .description('Run system health checks and diagnostics')
1467
+ .option('-j, --json', 'Output as JSON')
1468
+ .action(async (options) => {
1469
+ try {
1470
+ const fm = await initFlowMind();
1471
+ const result = await fm.doctor();
1472
+
1473
+ if (options.json) {
1474
+ console.log(JSON.stringify(result, null, 2));
1475
+ return;
1476
+ }
1477
+
1478
+ console.log(chalk.cyan('\nFlowMind Health Check\n'));
1479
+ console.log('─'.repeat(60));
1480
+
1481
+ for (const check of result.checks) {
1482
+ const icon = check.status === 'ok' ? chalk.green('✓')
1483
+ : check.status === 'warning' ? chalk.yellow('⚠')
1484
+ : chalk.red('✗');
1485
+ console.log(` ${icon} ${check.name.padEnd(20)} ${check.message}`);
1486
+ }
1487
+
1488
+ console.log('─'.repeat(60));
1489
+ const { ok, warnings, errors } = result.summary;
1490
+ const summaryLine = [
1491
+ chalk.green(`${ok} ok`),
1492
+ warnings > 0 ? chalk.yellow(`${warnings} warning(s)`) : null,
1493
+ errors > 0 ? chalk.red(`${errors} error(s)`) : null
1494
+ ].filter(Boolean).join(', ');
1495
+ console.log(`\n Result: ${summaryLine}`);
1496
+
1497
+ if (errors > 0) {
1498
+ console.log(chalk.red('\n Some checks failed. Please fix the errors above.'));
1499
+ } else if (warnings > 0) {
1500
+ console.log(chalk.yellow('\n Some warnings detected. Review for potential issues.'));
1501
+ } else {
1502
+ console.log(chalk.green('\n All checks passed!'));
1503
+ }
1504
+ } catch (error) {
1505
+ console.error(chalk.red('Doctor Error:'), error.message);
1506
+ }
1507
+ });
1508
+
1463
1509
  // Update command - Auto-update flowmind
1464
1510
  program
1465
1511
  .command('update')
@@ -8,48 +8,27 @@ class BaseModel {
8
8
  this.name = name;
9
9
  this.config = config;
10
10
  this.initialized = false;
11
+ this.requestTimeout = config.requestTimeout || 30000;
12
+ this.maxRetries = config.maxRetries || 3;
13
+ this.retryDelay = config.retryDelay || 1000;
11
14
  }
12
15
 
13
- /**
14
- * 初始化模型
15
- * @returns {Promise<void>}
16
- */
17
16
  async init() {
18
17
  throw new Error('init() must be implemented by subclass');
19
18
  }
20
19
 
21
- /**
22
- * 发送聊天请求
23
- * @param {Array} messages - 消息数组 [{role, content}]
24
- * @param {Object} options - 请求选项
25
- * @returns {Promise<string>} 模型响应
26
- */
27
20
  async chat(messages, options = {}) {
28
21
  throw new Error('chat() must be implemented by subclass');
29
22
  }
30
23
 
31
- /**
32
- * 发送补全请求
33
- * @param {string} prompt - 提示词
34
- * @param {Object} options - 请求选项
35
- * @returns {Promise<string>} 模型响应
36
- */
37
24
  async complete(prompt, options = {}) {
38
25
  throw new Error('complete() must be implemented by subclass');
39
26
  }
40
27
 
41
- /**
42
- * 检查模型是否可用
43
- * @returns {Promise<boolean>}
44
- */
45
28
  async isAvailable() {
46
29
  throw new Error('isAvailable() must be implemented by subclass');
47
30
  }
48
31
 
49
- /**
50
- * 获取模型信息
51
- * @returns {Object}
52
- */
53
32
  getInfo() {
54
33
  return {
55
34
  name: this.name,
@@ -58,13 +37,53 @@ class BaseModel {
58
37
  };
59
38
  }
60
39
 
61
- /**
62
- * 验证配置
63
- * @returns {boolean}
64
- */
65
40
  validateConfig() {
66
41
  return true;
67
42
  }
43
+
44
+ /**
45
+ * Fetch with retry, timeout, and exponential backoff
46
+ */
47
+ async fetchWithRetry(url, options = {}) {
48
+ const timeout = options.timeout || this.requestTimeout;
49
+ const maxRetries = options.retries || this.maxRetries;
50
+
51
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
52
+ try {
53
+ const controller = new AbortController();
54
+ const timer = setTimeout(() => controller.abort(), timeout);
55
+
56
+ const response = await fetch(url, {
57
+ ...options,
58
+ signal: controller.signal
59
+ });
60
+
61
+ clearTimeout(timer);
62
+
63
+ // Retry on rate limit (429) and server errors (5xx)
64
+ if (response.status === 429 || response.status >= 500) {
65
+ if (attempt < maxRetries) {
66
+ const delay = this.retryDelay * Math.pow(2, attempt);
67
+ await new Promise(r => setTimeout(r, delay));
68
+ continue;
69
+ }
70
+ }
71
+
72
+ return response;
73
+ } catch (error) {
74
+ if (error.name === 'AbortError') {
75
+ if (attempt < maxRetries) continue;
76
+ throw new Error(`Request timeout after ${timeout}ms`);
77
+ }
78
+ if (attempt < maxRetries) {
79
+ const delay = this.retryDelay * Math.pow(2, attempt);
80
+ await new Promise(r => setTimeout(r, delay));
81
+ continue;
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+ }
68
87
  }
69
88
 
70
89
  module.exports = BaseModel;
@@ -28,6 +28,9 @@ class ModelManager {
28
28
  };
29
29
  this.fallbackToRules = config.fallbackToRules !== false;
30
30
  this.initialized = false;
31
+ this.cache = new Map();
32
+ this.cacheMaxAge = config.cacheMaxAge || 300000; // 5 minutes
33
+ this.cacheMaxSize = config.cacheMaxSize || 100;
31
34
  }
32
35
 
33
36
  /**
@@ -119,30 +122,16 @@ class ModelManager {
119
122
  * @returns {Promise<Object>} 意图分析结果
120
123
  */
121
124
  async understandIntent(input, context = {}) {
122
- if (!this.features.intentUnderstanding) {
123
- return null;
124
- }
125
-
126
- const provider = this.getDefaultProvider();
127
- if (!provider) {
128
- return this._fallback('intentUnderstanding', input, context);
129
- }
125
+ if (!this.features.intentUnderstanding) return null;
130
126
 
131
- try {
127
+ return this._callWithFailover(async (provider) => {
132
128
  const messages = [
133
129
  { role: 'system', content: prompts.intent.system },
134
130
  { role: 'user', content: prompts.intent.user(input, context) }
135
131
  ];
136
-
137
- const response = await provider.chat(messages, {
138
- responseFormat: { type: 'json_object' }
139
- });
140
-
132
+ const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
141
133
  return JSON.parse(response);
142
- } catch (error) {
143
- console.warn(`AI intent understanding failed: ${error.message}`);
144
- return this._fallback('intentUnderstanding', input, context);
145
- }
134
+ }, 'intentUnderstanding');
146
135
  }
147
136
 
148
137
  /**
@@ -153,30 +142,16 @@ class ModelManager {
153
142
  * @returns {Promise<Object>} 提取的参数
154
143
  */
155
144
  async extractParameters(input, skillName, skillSchema = {}) {
156
- if (!this.features.parameterExtraction) {
157
- return {};
158
- }
145
+ if (!this.features.parameterExtraction) return {};
159
146
 
160
- const provider = this.getDefaultProvider();
161
- if (!provider) {
162
- return this._fallback('parameterExtraction', input, { skillName });
163
- }
164
-
165
- try {
147
+ return this._callWithFailover(async (provider) => {
166
148
  const messages = [
167
149
  { role: 'system', content: prompts.extraction.system },
168
150
  { role: 'user', content: prompts.extraction.user(input, skillName, skillSchema) }
169
151
  ];
170
-
171
- const response = await provider.chat(messages, {
172
- responseFormat: { type: 'json_object' }
173
- });
174
-
152
+ const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
175
153
  return JSON.parse(response);
176
- } catch (error) {
177
- console.warn(`AI parameter extraction failed: ${error.message}`);
178
- return this._fallback('parameterExtraction', input, { skillName });
179
- }
154
+ }, 'parameterExtraction');
180
155
  }
181
156
 
182
157
  /**
@@ -186,30 +161,16 @@ class ModelManager {
186
161
  * @returns {Promise<Object>} 选择的技能
187
162
  */
188
163
  async selectSkill(input, candidates) {
189
- if (!this.features.skillSelection) {
190
- return null;
191
- }
164
+ if (!this.features.skillSelection) return null;
192
165
 
193
- const provider = this.getDefaultProvider();
194
- if (!provider) {
195
- return this._fallback('skillSelection', input, { candidates });
196
- }
197
-
198
- try {
166
+ return this._callWithFailover(async (provider) => {
199
167
  const messages = [
200
168
  { role: 'system', content: prompts.selection.system },
201
169
  { role: 'user', content: prompts.selection.user(input, candidates) }
202
170
  ];
203
-
204
- const response = await provider.chat(messages, {
205
- responseFormat: { type: 'json_object' }
206
- });
207
-
171
+ const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
208
172
  return JSON.parse(response);
209
- } catch (error) {
210
- console.warn(`AI skill selection failed: ${error.message}`);
211
- return this._fallback('skillSelection', input, { candidates });
212
- }
173
+ }, 'skillSelection');
213
174
  }
214
175
 
215
176
  /**
@@ -219,26 +180,15 @@ class ModelManager {
219
180
  * @returns {Promise<string>} 摘要文本
220
181
  */
221
182
  async summarizeResult(result, context = {}) {
222
- if (!this.features.resultSummary) {
223
- return null;
224
- }
183
+ if (!this.features.resultSummary) return null;
225
184
 
226
- const provider = this.getDefaultProvider();
227
- if (!provider) {
228
- return this._fallback('resultSummary', result, context);
229
- }
230
-
231
- try {
185
+ return this._callWithFailover(async (provider) => {
232
186
  const messages = [
233
187
  { role: 'system', content: prompts.summary.system },
234
188
  { role: 'user', content: prompts.summary.user(result, context) }
235
189
  ];
236
-
237
190
  return await provider.chat(messages);
238
- } catch (error) {
239
- console.warn(`AI result summary failed: ${error.message}`);
240
- return this._fallback('resultSummary', result, context);
241
- }
191
+ }, 'resultSummary');
242
192
  }
243
193
 
244
194
  /**
@@ -248,42 +198,102 @@ class ModelManager {
248
198
  * @returns {Promise<Object>} 学习分析结果
249
199
  */
250
200
  async analyzeLearningFeedback(input, context = {}) {
251
- if (!this.features.learningFeedback) {
252
- return null;
253
- }
254
-
255
- const provider = this.getDefaultProvider();
256
- if (!provider) {
257
- return this._fallback('learningFeedback', input, context);
258
- }
201
+ if (!this.features.learningFeedback) return null;
259
202
 
260
- try {
203
+ return this._callWithFailover(async (provider) => {
261
204
  const messages = [
262
205
  { role: 'system', content: prompts.learning.system },
263
206
  { role: 'user', content: prompts.learning.user(input, context) }
264
207
  ];
208
+ const response = await provider.chat(messages, { responseFormat: { type: 'json_object' } });
209
+ return JSON.parse(response);
210
+ }, 'learningFeedback');
211
+ }
265
212
 
266
- const response = await provider.chat(messages, {
267
- responseFormat: { type: 'json_object' }
268
- });
213
+ /**
214
+ * Get cached result if available and not expired
215
+ * @private
216
+ */
217
+ _getCached(key) {
218
+ const entry = this.cache.get(key);
219
+ if (!entry) return null;
220
+ if (Date.now() - entry.timestamp > this.cacheMaxAge) {
221
+ this.cache.delete(key);
222
+ return null;
223
+ }
224
+ return entry.value;
225
+ }
269
226
 
270
- return JSON.parse(response);
271
- } catch (error) {
272
- console.warn(`AI learning feedback analysis failed: ${error.message}`);
273
- return this._fallback('learningFeedback', input, context);
227
+ /**
228
+ * Set cache entry, evicting oldest if at capacity
229
+ * @private
230
+ */
231
+ _setCache(key, value) {
232
+ if (this.cache.size >= this.cacheMaxSize) {
233
+ const oldest = this.cache.keys().next().value;
234
+ this.cache.delete(oldest);
235
+ }
236
+ this.cache.set(key, { value, timestamp: Date.now() });
237
+ }
238
+
239
+ /**
240
+ * Clear the AI response cache
241
+ */
242
+ clearCache() {
243
+ this.cache.clear();
244
+ }
245
+
246
+ /**
247
+ * Call AI with automatic failover to other providers
248
+ * @private
249
+ */
250
+ async _callWithFailover(fn, feature) {
251
+ // Try default provider first
252
+ const defaultProvider = this.getDefaultProvider();
253
+ if (defaultProvider) {
254
+ try {
255
+ return await fn(defaultProvider);
256
+ } catch (error) {
257
+ console.warn(`Default provider (${this.defaultProvider}) failed for ${feature}: ${error.message}`);
258
+ }
274
259
  }
260
+
261
+ // Try other providers
262
+ for (const [name, provider] of this.providers) {
263
+ if (name === this.defaultProvider || !provider.initialized) continue;
264
+ try {
265
+ console.warn(`Failing over to provider: ${name} for ${feature}`);
266
+ return await fn(provider);
267
+ } catch (error) {
268
+ console.warn(`Failover provider ${name} failed for ${feature}: ${error.message}`);
269
+ }
270
+ }
271
+
272
+ // All providers failed, use rule-based fallback
273
+ return this._fallback(feature);
275
274
  }
276
275
 
277
276
  /**
278
- * 降级处理
277
+ * Rule-based fallback when AI is unavailable
279
278
  * @private
280
279
  */
281
- _fallback(feature, input, context) {
282
- if (!this.fallbackToRules) {
283
- return null;
280
+ _fallback(feature) {
281
+ if (!this.fallbackToRules) return null;
282
+
283
+ switch (feature) {
284
+ case 'intentUnderstanding':
285
+ return { intent: 'unknown', confidence: 0, source: 'rules' };
286
+ case 'parameterExtraction':
287
+ return {};
288
+ case 'skillSelection':
289
+ return { selectedSkill: null, confidence: 0, source: 'rules' };
290
+ case 'resultSummary':
291
+ return null;
292
+ case 'learningFeedback':
293
+ return { isLearning: false, source: 'rules' };
294
+ default:
295
+ return null;
284
296
  }
285
- // 返回 null,让调用方使用规则引擎
286
- return null;
287
297
  }
288
298
 
289
299
  /**
@@ -8,7 +8,7 @@ class AnthropicProvider extends BaseModel {
8
8
  constructor(config = {}) {
9
9
  super('anthropic', config);
10
10
  this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
11
- this.model = config.model || 'claude-3-sonnet-20240229';
11
+ this.model = config.model || 'claude-sonnet-4-20250514';
12
12
  this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
13
13
  this.maxTokens = config.maxTokens ?? 2000;
14
14
  this.temperature = config.temperature ?? 0.3;
@@ -26,14 +26,10 @@ class AnthropicProvider extends BaseModel {
26
26
  }
27
27
 
28
28
  async chat(messages, options = {}) {
29
- if (!this.initialized) {
30
- await this.init();
31
- }
29
+ if (!this.initialized) await this.init();
32
30
 
33
- // 转换消息格式:提取 system 消息
34
31
  let systemPrompt = '';
35
32
  const userMessages = [];
36
-
37
33
  for (const msg of messages) {
38
34
  if (msg.role === 'system') {
39
35
  systemPrompt = msg.content;
@@ -42,7 +38,7 @@ class AnthropicProvider extends BaseModel {
42
38
  }
43
39
  }
44
40
 
45
- const response = await fetch(`${this.baseUrl}/v1/messages`, {
41
+ const response = await this.fetchWithRetry(`${this.baseUrl}/v1/messages`, {
46
42
  method: 'POST',
47
43
  headers: {
48
44
  'Content-Type': 'application/json',
@@ -74,19 +70,30 @@ class AnthropicProvider extends BaseModel {
74
70
  async isAvailable() {
75
71
  try {
76
72
  if (!this.apiKey) return false;
77
- // Anthropic 没有 models 端点,直接尝试调用
78
- return true;
73
+ // Anthropic has no /models endpoint; make a lightweight messages call to verify
74
+ const response = await this.fetchWithRetry(`${this.baseUrl}/v1/messages`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'x-api-key': this.apiKey,
79
+ 'anthropic-version': '2023-06-01'
80
+ },
81
+ body: JSON.stringify({
82
+ model: this.model,
83
+ max_tokens: 1,
84
+ messages: [{ role: 'user', content: 'hi' }]
85
+ }),
86
+ retries: 1,
87
+ timeout: 10000
88
+ });
89
+ return response.ok || response.status === 400; // 400 = bad request but API is reachable
79
90
  } catch {
80
91
  return false;
81
92
  }
82
93
  }
83
94
 
84
95
  getInfo() {
85
- return {
86
- ...super.getInfo(),
87
- model: this.model,
88
- baseUrl: this.baseUrl
89
- };
96
+ return { ...super.getInfo(), model: this.model, baseUrl: this.baseUrl };
90
97
  }
91
98
  }
92
99
 
@@ -31,7 +31,7 @@ class DeepSeekProvider extends BaseModel {
31
31
  await this.init();
32
32
  }
33
33
 
34
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
34
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
35
35
  method: 'POST',
36
36
  headers: {
37
37
  'Content-Type': 'application/json',
@@ -61,7 +61,17 @@ class DeepSeekProvider extends BaseModel {
61
61
  async isAvailable() {
62
62
  try {
63
63
  if (!this.apiKey) return false;
64
- return true;
64
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Authorization': `Bearer ${this.apiKey}`
69
+ },
70
+ body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
71
+ retries: 1,
72
+ timeout: 10000
73
+ });
74
+ return response.ok || response.status === 400;
65
75
  } catch {
66
76
  return false;
67
77
  }
@@ -26,7 +26,7 @@ class ERNIEProvider extends BaseModel {
26
26
  }
27
27
 
28
28
  async refreshAccessToken() {
29
- const response = await fetch(
29
+ const response = await this.fetchWithRetry(
30
30
  `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.secretKey}`,
31
31
  { method: 'POST' }
32
32
  );
@@ -51,7 +51,7 @@ class ERNIEProvider extends BaseModel {
51
51
  const model = options.model || this.model;
52
52
  const endpoint = this.getEndpoint(model);
53
53
 
54
- const response = await fetch(`${this.baseUrl}${endpoint}?access_token=${this.accessToken}`, {
54
+ const response = await this.fetchWithRetry(`${this.baseUrl}${endpoint}?access_token=${this.accessToken}`, {
55
55
  method: 'POST',
56
56
  headers: {
57
57
  'Content-Type': 'application/json'
@@ -31,7 +31,7 @@ class GLMProvider extends BaseModel {
31
31
  await this.init();
32
32
  }
33
33
 
34
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
34
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
35
35
  method: 'POST',
36
36
  headers: {
37
37
  'Content-Type': 'application/json',
@@ -61,7 +61,17 @@ class GLMProvider extends BaseModel {
61
61
  async isAvailable() {
62
62
  try {
63
63
  if (!this.apiKey) return false;
64
- return true;
64
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Authorization': `Bearer ${this.apiKey}`
69
+ },
70
+ body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
71
+ retries: 1,
72
+ timeout: 10000
73
+ });
74
+ return response.ok || response.status === 400;
65
75
  } catch {
66
76
  return false;
67
77
  }
@@ -31,7 +31,7 @@ class MiMoProvider extends BaseModel {
31
31
  await this.init();
32
32
  }
33
33
 
34
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
34
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
35
35
  method: 'POST',
36
36
  headers: {
37
37
  'Content-Type': 'application/json',
@@ -61,7 +61,17 @@ class MiMoProvider extends BaseModel {
61
61
  async isAvailable() {
62
62
  try {
63
63
  if (!this.apiKey) return false;
64
- return true;
64
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Authorization': `Bearer ${this.apiKey}`
69
+ },
70
+ body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
71
+ retries: 1,
72
+ timeout: 10000
73
+ });
74
+ return response.ok || response.status === 400;
65
75
  } catch {
66
76
  return false;
67
77
  }
@@ -34,7 +34,7 @@ class OllamaProvider extends BaseModel {
34
34
  // 转换消息格式为 Ollama 格式
35
35
  const prompt = this._convertMessagesToPrompt(messages);
36
36
 
37
- const response = await fetch(`${this.baseUrl}/api/generate`, {
37
+ const response = await this.fetchWithRetry(`${this.baseUrl}/api/generate`, {
38
38
  method: 'POST',
39
39
  headers: {
40
40
  'Content-Type': 'application/json'
@@ -64,7 +64,7 @@ class OllamaProvider extends BaseModel {
64
64
  await this.init();
65
65
  }
66
66
 
67
- const response = await fetch(`${this.baseUrl}/api/generate`, {
67
+ const response = await this.fetchWithRetry(`${this.baseUrl}/api/generate`, {
68
68
  method: 'POST',
69
69
  headers: {
70
70
  'Content-Type': 'application/json'
@@ -91,9 +91,10 @@ class OllamaProvider extends BaseModel {
91
91
 
92
92
  async isAvailable() {
93
93
  try {
94
- const response = await fetch(`${this.baseUrl}/api/tags`, {
94
+ const response = await this.fetchWithRetry(`${this.baseUrl}/api/tags`, {
95
95
  method: 'GET',
96
- signal: AbortSignal.timeout(5000) // 5秒超时
96
+ retries: 1,
97
+ timeout: 5000
97
98
  });
98
99
  return response.ok;
99
100
  } catch {
@@ -8,7 +8,7 @@ class OpenAIProvider extends BaseModel {
8
8
  constructor(config = {}) {
9
9
  super('openai', config);
10
10
  this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
11
- this.model = config.model || 'gpt-4';
11
+ this.model = config.model || 'gpt-4o';
12
12
  this.baseUrl = config.baseUrl || 'https://api.openai.com/v1';
13
13
  this.temperature = config.temperature ?? 0.3;
14
14
  this.maxTokens = config.maxTokens ?? 2000;
@@ -26,11 +26,9 @@ class OpenAIProvider extends BaseModel {
26
26
  }
27
27
 
28
28
  async chat(messages, options = {}) {
29
- if (!this.initialized) {
30
- await this.init();
31
- }
29
+ if (!this.initialized) await this.init();
32
30
 
33
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
31
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
34
32
  method: 'POST',
35
33
  headers: {
36
34
  'Content-Type': 'application/json',
@@ -61,8 +59,10 @@ class OpenAIProvider extends BaseModel {
61
59
  async isAvailable() {
62
60
  try {
63
61
  if (!this.apiKey) return false;
64
- const response = await fetch(`${this.baseUrl}/models`, {
65
- headers: { 'Authorization': `Bearer ${this.apiKey}` }
62
+ const response = await this.fetchWithRetry(`${this.baseUrl}/models`, {
63
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
64
+ retries: 1,
65
+ timeout: 10000
66
66
  });
67
67
  return response.ok;
68
68
  } catch {
@@ -71,11 +71,7 @@ class OpenAIProvider extends BaseModel {
71
71
  }
72
72
 
73
73
  getInfo() {
74
- return {
75
- ...super.getInfo(),
76
- model: this.model,
77
- baseUrl: this.baseUrl
78
- };
74
+ return { ...super.getInfo(), model: this.model, baseUrl: this.baseUrl };
79
75
  }
80
76
  }
81
77
 
@@ -31,7 +31,7 @@ class QwenProvider extends BaseModel {
31
31
  await this.init();
32
32
  }
33
33
 
34
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
34
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
35
35
  method: 'POST',
36
36
  headers: {
37
37
  'Content-Type': 'application/json',
@@ -61,7 +61,17 @@ class QwenProvider extends BaseModel {
61
61
  async isAvailable() {
62
62
  try {
63
63
  if (!this.apiKey) return false;
64
- return true;
64
+ const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Authorization': `Bearer ${this.apiKey}`
69
+ },
70
+ body: JSON.stringify({ model: this.model, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }),
71
+ retries: 1,
72
+ timeout: 10000
73
+ });
74
+ return response.ok || response.status === 400;
65
75
  } catch {
66
76
  return false;
67
77
  }
@@ -50,7 +50,7 @@ class HonorEngine {
50
50
  }
51
51
  this.initialized = true;
52
52
  } catch (error) {
53
- // Non-blocking: create default data in memory
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
- // Non-blocking
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
- // Non-blocking
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
- // Non-blocking
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
- // Non-blocking
250
+ console.warn('HonorEngine save failed:', error.message);
251
251
  }
252
252
  }
253
253
  }
package/core/index.js CHANGED
@@ -22,6 +22,8 @@ class FlowMind {
22
22
  this.skills = new SkillLoader(this.config, this.learning, this.components, this.honor);
23
23
  this.ai = new ModelManager(options.ai || {});
24
24
  this.initialized = false;
25
+ this.conversationHistory = [];
26
+ this.maxHistoryLength = options.maxHistoryLength || 50;
25
27
  }
26
28
 
27
29
  /**
@@ -31,6 +33,13 @@ class FlowMind {
31
33
  if (this.initialized) return this;
32
34
 
33
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
+
34
43
  await this.components.init();
35
44
  await this.components.initAll();
36
45
  await this.honor.init();
@@ -49,6 +58,19 @@ class FlowMind {
49
58
  return this;
50
59
  }
51
60
 
61
+ /**
62
+ * Process a user request with streaming support
63
+ * Yields progress updates as an async generator
64
+ */
65
+ async *processStream(input, context = {}) {
66
+ yield { type: 'start', input, timestamp: new Date().toISOString() };
67
+
68
+ const result = await this.process(input, context);
69
+
70
+ yield { type: 'progress', message: `Skill: ${result.metadata?.skill || 'unknown'}`, timestamp: new Date().toISOString() };
71
+ yield { type: 'result', data: result, timestamp: new Date().toISOString() };
72
+ }
73
+
52
74
  /**
53
75
  * Process a user request
54
76
  */
@@ -57,20 +79,31 @@ class FlowMind {
57
79
  await this.init();
58
80
  }
59
81
 
82
+ if (!input || typeof input !== 'string') {
83
+ return this.formatError('Invalid input: expected a non-empty string', input);
84
+ }
85
+
60
86
  const startTime = Date.now();
61
87
 
88
+ // Build context with conversation history
89
+ const enhancedContext = {
90
+ ...context,
91
+ conversationHistory: this.conversationHistory.slice(-10),
92
+ sessionId: context.sessionId || 'default'
93
+ };
94
+
62
95
  eventBus.emit('process:start', { input, timestamp: new Date().toISOString() });
63
96
 
64
97
  try {
65
98
  // 1. AI Intent Understanding (if available)
66
- const intent = await this.ai.understandIntent(input, context);
99
+ const intent = await this.ai.understandIntent(input, enhancedContext);
67
100
 
68
101
  // 2. Check for learning patterns (corrections, feedback)
69
102
  // Use AI to analyze learning feedback if available
70
- const aiLearningResult = await this.ai.analyzeLearningFeedback(input, context);
103
+ const aiLearningResult = await this.ai.analyzeLearningFeedback(input, enhancedContext);
71
104
  const learningResult = aiLearningResult?.isLearning
72
105
  ? aiLearningResult
73
- : await this.learning.detectLearning(input, context);
106
+ : await this.learning.detectLearning(input, enhancedContext);
74
107
  if (learningResult) {
75
108
  return this.formatLearningResponse(learningResult);
76
109
  }
@@ -106,12 +139,12 @@ class FlowMind {
106
139
  const extractedParams = await this.ai.extractParameters(input, skill.name);
107
140
 
108
141
  // 6. Execute with learning applied
109
- const enhancedContext = {
110
- ...context,
142
+ const executeContext = {
143
+ ...enhancedContext,
111
144
  ...extractedParams,
112
145
  intent: intent
113
146
  };
114
- const result = await this.executeWithLearning(skill, input, enhancedContext);
147
+ const result = await this.executeWithLearning(skill, input, executeContext);
115
148
 
116
149
  // 7. Generate AI summary (if available)
117
150
  const summary = await this.ai.summarizeResult(result, {
@@ -128,6 +161,17 @@ class FlowMind {
128
161
  aiEnhanced: !!summary
129
162
  });
130
163
 
164
+ // Save to conversation history
165
+ this.conversationHistory.push({
166
+ input,
167
+ output: formatted,
168
+ skill: skill.name,
169
+ timestamp: new Date().toISOString()
170
+ });
171
+ if (this.conversationHistory.length > this.maxHistoryLength) {
172
+ this.conversationHistory.shift();
173
+ }
174
+
131
175
  eventBus.emit('process:complete', {
132
176
  input,
133
177
  skill: skill.name,
@@ -320,6 +364,146 @@ class FlowMind {
320
364
  async importLearnings(data) {
321
365
  return this.learning.import(data);
322
366
  }
367
+
368
+ /**
369
+ * Get conversation history
370
+ */
371
+ getConversationHistory(sessionId) {
372
+ if (sessionId) {
373
+ return this.conversationHistory.filter(h => h.sessionId === sessionId);
374
+ }
375
+ return this.conversationHistory;
376
+ }
377
+
378
+ /**
379
+ * Clear conversation history
380
+ */
381
+ clearHistory() {
382
+ this.conversationHistory = [];
383
+ }
384
+
385
+ /**
386
+ * Graceful shutdown - flush pending data and clean up
387
+ */
388
+ async shutdown() {
389
+ eventBus.emit('shutdown:start', { timestamp: new Date().toISOString() });
390
+
391
+ // Save learning data
392
+ try {
393
+ if (this.learning?.skillBindings) {
394
+ await this.learning.saveSkillBindings();
395
+ }
396
+ if (this.learning?.stats) {
397
+ await this.learning.saveStats();
398
+ }
399
+ } catch (e) {
400
+ console.warn('Failed to save learning data during shutdown:', e.message);
401
+ }
402
+
403
+ // Save honor data
404
+ try {
405
+ if (this.honor?.save) {
406
+ await this.honor.save();
407
+ }
408
+ } catch (e) {
409
+ console.warn('Failed to save honor data during shutdown:', e.message);
410
+ }
411
+
412
+ // Clear conversation history
413
+ this.conversationHistory = [];
414
+
415
+ this.initialized = false;
416
+ eventBus.emit('shutdown:complete', { timestamp: new Date().toISOString() });
417
+ }
418
+
419
+ /**
420
+ * Run system health checks (doctor)
421
+ */
422
+ async doctor() {
423
+ const checks = [];
424
+
425
+ // 1. Config check
426
+ try {
427
+ await this.config.load();
428
+ checks.push({ name: 'Configuration', status: 'ok', message: 'Config loaded successfully' });
429
+ } catch (e) {
430
+ checks.push({ name: 'Configuration', status: 'error', message: e.message });
431
+ }
432
+
433
+ // 2. Skills check
434
+ try {
435
+ const skills = this.skills.list();
436
+ checks.push({ name: 'Skills', status: 'ok', message: `${skills.length} skill(s) loaded` });
437
+ } catch (e) {
438
+ checks.push({ name: 'Skills', status: 'error', message: e.message });
439
+ }
440
+
441
+ // 3. Learning engine check
442
+ try {
443
+ const stats = await this.learning.getStats();
444
+ checks.push({ name: 'Learning Engine', status: 'ok', message: `${stats.totalRecords || 0} records` });
445
+ } catch (e) {
446
+ checks.push({ name: 'Learning Engine', status: 'error', message: e.message });
447
+ }
448
+
449
+ // 4. Honor engine check
450
+ try {
451
+ const honor = this.honor.getData();
452
+ checks.push({ name: 'Honor Engine', status: 'ok', message: `Level ${honor.level}, ${honor.points} pts` });
453
+ } catch (e) {
454
+ checks.push({ name: 'Honor Engine', status: 'error', message: e.message });
455
+ }
456
+
457
+ // 5. AI providers check
458
+ try {
459
+ const aiStatus = this.ai.getStatus();
460
+ const providerCount = Object.keys(aiStatus.providers || {}).length;
461
+ const activeCount = Object.values(aiStatus.providers || {}).filter(p => p.initialized).length;
462
+ checks.push({ name: 'AI Providers', status: activeCount > 0 ? 'ok' : 'warning', message: `${activeCount}/${providerCount} active` });
463
+ } catch (e) {
464
+ checks.push({ name: 'AI Providers', status: 'warning', message: e.message });
465
+ }
466
+
467
+ // 6. Components check
468
+ try {
469
+ const compStatus = this.components.getStatus();
470
+ const compCount = Object.keys(compStatus).length;
471
+ checks.push({ name: 'Components', status: 'ok', message: `${compCount} registered` });
472
+ } catch (e) {
473
+ checks.push({ name: 'Components', status: 'warning', message: e.message });
474
+ }
475
+
476
+ // 7. Node.js version check
477
+ const nodeVersion = process.version;
478
+ const major = parseInt(nodeVersion.slice(1));
479
+ checks.push({
480
+ name: 'Node.js',
481
+ status: major >= 18 ? 'ok' : 'warning',
482
+ message: `${nodeVersion} ${major < 18 ? '(recommend >= 18)' : ''}`
483
+ });
484
+
485
+ // 8. Disk space for learning data
486
+ try {
487
+ const fs = require('fs-extra');
488
+ const learningPath = this.learning.expandPath(this.learning.learningPath);
489
+ if (await fs.pathExists(learningPath)) {
490
+ checks.push({ name: 'Learning Storage', status: 'ok', message: learningPath });
491
+ } else {
492
+ checks.push({ name: 'Learning Storage', status: 'warning', message: 'Directory not yet created' });
493
+ }
494
+ } catch (e) {
495
+ checks.push({ name: 'Learning Storage', status: 'warning', message: e.message });
496
+ }
497
+
498
+ const errors = checks.filter(c => c.status === 'error').length;
499
+ const warnings = checks.filter(c => c.status === 'warning').length;
500
+
501
+ return {
502
+ healthy: errors === 0,
503
+ checks,
504
+ summary: { total: checks.length, ok: checks.filter(c => c.status === 'ok').length, warnings, errors }
505
+ };
506
+ }
323
507
  }
324
508
 
325
509
  module.exports = FlowMind;
@@ -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
- if (filePath.startsWith('~')) {
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) {
@@ -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
- if (filePath.startsWith('~')) {
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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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
@@ -26,7 +26,7 @@ function App({ flowmind }) {
26
26
  if (!mountedRef.current) return;
27
27
  if (result.type === 'result') {
28
28
  const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
29
- addResponse(text.substring(0, 200) + (text.length > 200 ? '...' : ''));
29
+ addResponse(text);
30
30
  } else if (result.type === 'learning') {
31
31
  addResponse(result.message || 'Learning recorded');
32
32
  } else if (result.type === 'error') {
@@ -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
6
  function ChatPanel({ onSubmit, isProcessing, onExit }) {
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
19
+ useInput((ch, key) => {
20
+ if (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();