flowmind 1.5.2 → 1.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.5.4] - 2026-07-01
6
+
7
+ ### Added
8
+ - FlowMind now resolves deferred MCP invocations from skill outputs and executes them through the registered adapter when available
9
+ - Auto-flow, YApi, Yuque, and log audit skills now return concise execution summaries instead of raw MCP payload shapes
10
+
11
+ ### Fixed
12
+ - Repeated TUI submissions are now serialized to avoid multi-input crashes during fast follow-up commands
13
+ - CLI startup now falls back to ASCII-friendly banners in managed terminal environments that can crash on full graphics rendering
14
+
15
+ ### Changed
16
+ - `resource-bind` and related learned bindings now surface clearer resource summaries for follow-up skill reuse
17
+ - Result formatting now unwraps nested execution payloads so the TUI shows the actual output instead of intermediate envelopes
18
+
5
19
  ## [1.5.2] - 2026-07-01
6
20
 
7
21
  ### Added
package/bin/flowmind.js CHANGED
@@ -75,7 +75,19 @@ async function initFlowMind(options = {}) {
75
75
  /**
76
76
  * Display banner
77
77
  */
78
- function showBanner() {
78
+ function showBanner(asciiMode = false) {
79
+ if (asciiMode) {
80
+ console.log(chalk.cyan(`
81
+ +--------------------------------------------------+
82
+ | |
83
+ | FlowMind |
84
+ | The AI Agent That Learns How You Work |
85
+ | |
86
+ +--------------------------------------------------+
87
+ `));
88
+ return;
89
+ }
90
+
79
91
  console.log(chalk.cyan(`
80
92
  ╔══════════════════════════════════════════════════╗
81
93
  ║ ║
@@ -89,7 +101,71 @@ function showBanner() {
89
101
  /**
90
102
  * Dragon Totem ASCII Art by level - Chinese Dragon (中国龙)
91
103
  */
92
- function getDragonArt(level) {
104
+ function getDragonArt(level, asciiMode = false) {
105
+ if (asciiMode) {
106
+ const asciiArts = {
107
+ 0: [
108
+ ' /-----\\ ',
109
+ ' / /-\\ \\ ',
110
+ ' | | o | | ',
111
+ ' | | | | ',
112
+ ' | \\_/ | ',
113
+ ' \\ / ',
114
+ ' \\-----/ ',
115
+ ' Dragon Egg '
116
+ ],
117
+ 1: [
118
+ ' /--\\ ',
119
+ ' /----/ \\---\\ ',
120
+ ' / o \\/ \\ ',
121
+ ' \\ /\\ /\\ / ',
122
+ ' \\/\\/ \\/\\/ \\/ ',
123
+ ' Hatchling Dragon '
124
+ ],
125
+ 2: [
126
+ ' /-\\ /-\\ ',
127
+ ' /----/ \\/ \\---\\ ',
128
+ ' / o __ \\ ',
129
+ ' \\ /--------\\ / ',
130
+ ' \\---/ ////// \\/ ',
131
+ ' \\_/ \\_/ ',
132
+ ' Juvenile Dragon '
133
+ ],
134
+ 3: [
135
+ ' /---\\ /---\\ ',
136
+ ' /---/ \\/ \\---\\ ',
137
+ '/ o __ \\ ',
138
+ '| /----------\\ | ',
139
+ ' \\--/ //////// \\-/ ',
140
+ ' \\/ ////////// \\/ ',
141
+ ' \\_/ \\_/ ',
142
+ ' Adult Dragon '
143
+ ],
144
+ 4: [
145
+ ' /---\\ /---\\ ',
146
+ '/---/ \\____/ \\---\\ ',
147
+ '| o __ | ',
148
+ '| /------------\\ | ',
149
+ ' \\-/ //////////// \\-/ ',
150
+ ' / //////////// \\ ',
151
+ ' /_/ \\_\\ ',
152
+ ' Elder Dragon '
153
+ ],
154
+ 5: [
155
+ ' * /---\\ /---\\ * ',
156
+ '/-/ \\______/ \\-\\ ',
157
+ '| o __ | ',
158
+ '| /--------------\\ | ',
159
+ ' \\-/ /////**///// \\-/ ',
160
+ ' / ///// ///// \\ ',
161
+ ' /_/ ** ** \\_\\ ',
162
+ ' * \\______________/ * ',
163
+ ' Ascended Dragon '
164
+ ]
165
+ };
166
+ return asciiArts[level] || asciiArts[0];
167
+ }
168
+
93
169
  const arts = {
94
170
  0: [
95
171
  ' ╭─────╮ ',
@@ -192,11 +268,11 @@ function getHighlightColor(level) {
192
268
  /**
193
269
  * Render dragon totem with honor data
194
270
  */
195
- function renderDragonTotem(honorData) {
271
+ function renderDragonTotem(honorData, asciiMode = false) {
196
272
  const info = getLevelInfo(honorData.points);
197
273
  const dragonColor = getDragonColor(info.level);
198
274
  const highlightColor = getHighlightColor(info.level);
199
- const art = getDragonArt(info.level);
275
+ const art = getDragonArt(info.level, asciiMode);
200
276
 
201
277
  // Calculate max art line width for proper padding
202
278
  const maxArtWidth = Math.max(...art.map(l => l.length));
@@ -210,7 +286,8 @@ function renderDragonTotem(honorData) {
210
286
 
211
287
  console.log('');
212
288
  console.log(chalk.cyan(' ┌' + border + '┐'));
213
- console.log(chalk.cyan(' │') + ' 🐉 Dragon Totem of Honor ' + padRight(' 🐉 Dragon Totem of Honor ', boxWidth - 2) + chalk.cyan('│'));
289
+ const title = asciiMode ? ' Dragon Totem of Honor ' : ' 🐉 Dragon Totem of Honor ';
290
+ console.log(chalk.cyan(' │') + title + padRight(title, boxWidth - 2) + chalk.cyan('│'));
214
291
  console.log(chalk.cyan(' ├' + border + '┤'));
215
292
 
216
293
  for (const line of art) {
@@ -366,7 +443,7 @@ program
366
443
  .description('Initialize FlowMind in current directory')
367
444
  .option('--ai <provider>', 'Initialize with AI provider (openai/anthropic/glm/mimo/qwen/ernie/deepseek/ollama)')
368
445
  .action(async (options) => {
369
- showBanner();
446
+ showBanner(options.ascii || shouldUseAsciiUi());
370
447
 
371
448
  const spinner = ora('Initializing FlowMind...').start();
372
449
 
@@ -836,7 +913,7 @@ program
836
913
  } else if (options.publish) {
837
914
  await publishHonor({ output: options.output, gist: options.gist });
838
915
  } else {
839
- renderDragonTotem(honorData);
916
+ renderDragonTotem(honorData, shouldUseAsciiUi());
840
917
  console.log(chalk.gray(` ${getNextLevelHint(honorData.points)}`));
841
918
  }
842
919
  } catch (error) {
@@ -959,7 +1036,7 @@ program
959
1036
 
960
1037
  // Interactive mode
961
1038
  async function runInteractiveMode(fm) {
962
- showBanner();
1039
+ showBanner(shouldUseAsciiUi());
963
1040
  console.log(chalk.cyan('Interactive mode started. Type "exit" to quit.\n'));
964
1041
 
965
1042
  try {
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const BaseAdapter = require('./base-adapter');
7
+ const { callMcpTool } = require('../mcp-http-client');
7
8
 
8
9
  class McpAdapter extends BaseAdapter {
9
10
  constructor(providerName, config = {}) {
@@ -60,6 +61,31 @@ class McpAdapter extends BaseAdapter {
60
61
  tools: this.getToolMappings()
61
62
  };
62
63
  }
64
+
65
+ getTransportConfig() {
66
+ return this.config.transport || this.config.mcpServerConfig || this.config.mcpTransport || {};
67
+ }
68
+
69
+ async callTool(tool, args = {}) {
70
+ const transport = this.getTransportConfig();
71
+ const serverName = this.mcpServer || this.providerName;
72
+ return callMcpTool({
73
+ url: transport.url || transport.endpoint || transport.baseUrl,
74
+ headers: transport.headers || {},
75
+ tool,
76
+ args,
77
+ timeoutMs: transport.timeoutMs || 60000,
78
+ serverName
79
+ });
80
+ }
81
+
82
+ async callMcpTool(localName, args = {}) {
83
+ const tool = this.resolveTool(localName);
84
+ if (!tool) {
85
+ throw new Error(`Unknown MCP tool mapping: ${localName}`);
86
+ }
87
+ return this.callTool(tool, args);
88
+ }
63
89
  }
64
90
 
65
91
  module.exports = McpAdapter;
@@ -29,6 +29,32 @@ class WorkflowAdapter extends McpAdapter {
29
29
  throw new Error('Subclasses must implement listPipelines()');
30
30
  }
31
31
 
32
+ /**
33
+ * Query the current iterate metadata used by workflow lookup flows.
34
+ * @returns {Promise<object>}
35
+ */
36
+ async getCurrentIterate() {
37
+ throw new Error('Subclasses must implement getCurrentIterate()');
38
+ }
39
+
40
+ /**
41
+ * List deploy checklist records that contain service-to-pipeline mappings.
42
+ * @param {object} params
43
+ * @returns {Promise<object>}
44
+ */
45
+ async listDeployChecklists(params) {
46
+ throw new Error('Subclasses must implement listDeployChecklists()');
47
+ }
48
+
49
+ /**
50
+ * Query historical flow orders that can be used as a fallback pipeline source.
51
+ * @param {object} params
52
+ * @returns {Promise<object>}
53
+ */
54
+ async orderList(params) {
55
+ throw new Error('Subclasses must implement orderList()');
56
+ }
57
+
32
58
  /**
33
59
  * Start a pipeline run.
34
60
  * @param {string} pipelineId
@@ -82,7 +82,7 @@ class MiMoProvider extends BaseModel {
82
82
  ...super.getInfo(),
83
83
  model: this.model,
84
84
  baseUrl: this.baseUrl,
85
- provider: 'Xiaomi'
85
+ provider: 'MiMo'
86
86
  };
87
87
  }
88
88
  }
@@ -41,6 +41,10 @@ class ComponentRegistry {
41
41
  const AliyunDmsAdapter = require('./providers/aliyun/dms-adapter');
42
42
  return new AliyunDmsAdapter(this.getProviderConfig('databaseManager', 'aliyun-dms'));
43
43
  });
44
+ this.registerFactory('aliyun-rds-query', () => {
45
+ const AliyunRdsQueryAdapter = require('./providers/aliyun/rds-query-adapter');
46
+ return new AliyunRdsQueryAdapter(this.getProviderConfig('databaseQuery', 'aliyun-rds-query'));
47
+ });
44
48
  this.registerFactory('aliyun-redis', () => {
45
49
  const AliyunRedisAdapter = require('./providers/aliyun/redis-adapter');
46
50
  return new AliyunRedisAdapter(this.getProviderConfig('redisMonitor', 'aliyun-redis'));
@@ -79,7 +83,21 @@ class ComponentRegistry {
79
83
  const components = this.config.get('components', {});
80
84
  const typeConfig = components[componentType] || {};
81
85
  const providers = typeConfig.providers || {};
82
- return providers[providerName] || {};
86
+ const baseConfig = providers[providerName] || {};
87
+ const mcpServerName = McpCompatibility.getMcpServer(componentType, providerName);
88
+ const mcpServers = this.config.get('mcpServers', {});
89
+ const transport = mcpServerName ? mcpServers[mcpServerName] : null;
90
+
91
+ if (!transport) {
92
+ return baseConfig;
93
+ }
94
+
95
+ return {
96
+ ...baseConfig,
97
+ transport: {
98
+ ...transport
99
+ }
100
+ };
83
101
  }
84
102
 
85
103
  /**
@@ -82,7 +82,10 @@ class ConfigManager {
82
82
  if (await fs.pathExists(resourceConfigPath)) {
83
83
  try {
84
84
  const resourceConfig = await fs.readJson(resourceConfigPath);
85
- this.mergeConfig({ resources: resourceConfig.resources || {} });
85
+ this.mergeConfig({
86
+ resources: resourceConfig.resources || {},
87
+ mcpServers: resourceConfig.mcpServers || {}
88
+ });
86
89
  } catch (error) {
87
90
  console.warn('Failed to load resource config:', error.message);
88
91
  }
@@ -216,7 +219,9 @@ class ConfigManager {
216
219
  redis: { enabled: false },
217
220
  logs: { enabled: false },
218
221
  apiDocs: { enabled: false }
219
- }
222
+ },
223
+
224
+ mcpServers: {}
220
225
  };
221
226
  }
222
227
 
package/core/index.js CHANGED
@@ -11,6 +11,8 @@ const ConfigManager = require('./config-manager');
11
11
  const ComponentRegistry = require('./component-registry');
12
12
  const ModelManager = require('./ai/model-manager');
13
13
  const eventBus = require('./event-bus');
14
+ const { autoSyncSddAgentToFlowMind } = require('./sdd-agent-sync');
15
+ const { inferSourceContext } = require('./source-inference');
14
16
 
15
17
  class FlowMind {
16
18
  constructor(options = {}) {
@@ -32,6 +34,7 @@ class FlowMind {
32
34
  async init() {
33
35
  if (this.initialized) return this;
34
36
 
37
+ await autoSyncSddAgentToFlowMind();
35
38
  await this.config.load();
36
39
 
37
40
  // Validate configuration
@@ -191,19 +194,30 @@ class FlowMind {
191
194
  // Get learning rules for this skill
192
195
  const learnings = await this.learning.getSkillLearnings(skill.name);
193
196
  const resourceBinding = await this.learning.matchResourceBinding(input, skill.name);
197
+ let sourceInference = context.sourceInference || null;
198
+ if (!sourceInference) {
199
+ try {
200
+ sourceInference = await inferSourceContext(input);
201
+ } catch (error) {
202
+ sourceInference = null;
203
+ }
204
+ }
194
205
 
195
206
  // Apply learning rules to context
196
207
  const enhancedContext = {
197
208
  ...context,
198
209
  flowmind: context.flowmind || this,
210
+ componentRegistry: context.componentRegistry || this.components,
199
211
  currentSkill: context.currentSkill || skill.name,
200
212
  learnings: learnings,
201
213
  preferences: await this.learning.getPreferences(skill.name),
202
- resourceBinding
214
+ resourceBinding,
215
+ sourceInference
203
216
  };
204
217
 
205
218
  // Execute skill
206
219
  const result = await skill.execute(input, enhancedContext);
220
+ const resolvedResult = await this.resolveDeferredMcpResult(result, skill, enhancedContext);
207
221
 
208
222
  if (resourceBinding?.id) {
209
223
  await this.learning.markResourceBindingUsed(resourceBinding.id, {
@@ -226,7 +240,144 @@ class FlowMind {
226
240
  // Award honor point for skill use
227
241
  await this.honor.award('skill_use', `Used skill: ${skill.name}`);
228
242
 
229
- return result;
243
+ return resolvedResult;
244
+ }
245
+
246
+ async resolveDeferredMcpResult(result, skill, context = {}) {
247
+ const invocation = this.extractMcpInvocation(result);
248
+ if (!invocation) {
249
+ return result;
250
+ }
251
+
252
+ const adapter = this.resolveDeferredAdapter(invocation, skill, context);
253
+ if (!adapter) {
254
+ return result;
255
+ }
256
+
257
+ const executor = typeof adapter.callTool === 'function'
258
+ ? adapter.callTool.bind(adapter)
259
+ : typeof adapter.callMcpTool === 'function'
260
+ ? adapter.callMcpTool.bind(adapter)
261
+ : null;
262
+
263
+ if (!executor) {
264
+ return result;
265
+ }
266
+
267
+ try {
268
+ const execution = await executor(invocation.tool, invocation.args || {});
269
+ const resultData = result && typeof result === 'object' && !Array.isArray(result) && result.data && typeof result.data === 'object'
270
+ ? result.data
271
+ : {};
272
+
273
+ return {
274
+ ...result,
275
+ data: {
276
+ ...resultData,
277
+ execution,
278
+ mcpTool: invocation.tool,
279
+ mcpExecuted: true,
280
+ mcpServer: invocation.mcpServer || adapter.mcpServer || adapter.providerName
281
+ }
282
+ };
283
+ } catch (error) {
284
+ return {
285
+ ...result,
286
+ type: result?.type || 'error',
287
+ success: false,
288
+ message: result?.message || error.message,
289
+ data: {
290
+ ...(result && typeof result === 'object' && result.data && typeof result.data === 'object' ? result.data : {}),
291
+ mcpTool: invocation.tool,
292
+ mcpExecuted: false,
293
+ mcpError: error.message
294
+ }
295
+ };
296
+ }
297
+ }
298
+
299
+ extractMcpInvocation(result) {
300
+ if (!result || typeof result !== 'object') {
301
+ return null;
302
+ }
303
+
304
+ const candidates = [
305
+ result.data,
306
+ result
307
+ ];
308
+
309
+ for (const candidate of candidates) {
310
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
311
+ continue;
312
+ }
313
+
314
+ const tool = candidate.mcpTool || candidate.tool;
315
+ if (!tool || typeof tool !== 'string') {
316
+ continue;
317
+ }
318
+
319
+ const args = candidate.args || candidate.params || {};
320
+ if (args && typeof args !== 'object') {
321
+ continue;
322
+ }
323
+
324
+ return {
325
+ tool,
326
+ args,
327
+ mcpServer: candidate.mcpServer || null
328
+ };
329
+ }
330
+
331
+ return null;
332
+ }
333
+
334
+ resolveDeferredAdapter(invocation, skill, context = {}) {
335
+ const registry = context.componentRegistry || this.components;
336
+ if (!registry) {
337
+ return null;
338
+ }
339
+
340
+ const bindingComponentType = context.resourceBinding?.componentType || context.resolvedBinding?.componentType || null;
341
+ const skillComponentType = this.inferSkillComponentType(skill, context);
342
+ const componentTypes = [
343
+ bindingComponentType,
344
+ skillComponentType,
345
+ invocation.componentType
346
+ ].filter(Boolean);
347
+
348
+ for (const componentType of componentTypes) {
349
+ const adapter = registry.getAdapter(componentType);
350
+ if (adapter) {
351
+ return adapter;
352
+ }
353
+ }
354
+
355
+ if (typeof registry.getAll === 'function') {
356
+ const allAdapters = registry.getAll();
357
+ for (const entry of allAdapters) {
358
+ const adapter = registry.getAdapterByProvider?.(entry.type, entry.provider) || null;
359
+ if (adapter && (adapter.mcpServer === invocation.mcpServer || adapter.providerName === invocation.provider)) {
360
+ return adapter;
361
+ }
362
+ }
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ inferSkillComponentType(skill, context = {}) {
369
+ const name = skill?.name || context.currentSkill || '';
370
+ const mapping = {
371
+ 'log-audit': 'logService',
372
+ 'sls-log-audit': 'logService',
373
+ 'yapi-sync-interface': 'apiDoc',
374
+ 'yuque-sync-design': 'knowledgeBase',
375
+ 'data-logic-validation': 'databaseQuery',
376
+ 'resource-bind': null,
377
+ 'auto-flow': 'workflow'
378
+ };
379
+
380
+ return mapping[name] || null;
230
381
  }
231
382
 
232
383
  /**
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Minimal JSON-RPC MCP HTTP client.
3
+ * Sends tools/call requests to an MCP gateway over HTTP.
4
+ */
5
+
6
+ async function callMcpTool({ url, headers = {}, tool, args = {}, timeoutMs = 60000, serverName = 'mcp' }) {
7
+ if (!url) {
8
+ throw new Error(`MCP transport URL not configured for ${serverName}`);
9
+ }
10
+
11
+ const requestId = Date.now();
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
14
+
15
+ try {
16
+ const response = await fetch(url, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ 'User-Agent': 'flowmind',
21
+ ...headers
22
+ },
23
+ body: JSON.stringify({
24
+ jsonrpc: '2.0',
25
+ id: requestId,
26
+ method: 'tools/call',
27
+ params: {
28
+ name: tool,
29
+ arguments: args
30
+ }
31
+ }),
32
+ signal: controller.signal
33
+ });
34
+
35
+ const text = await response.text();
36
+ let payload = null;
37
+
38
+ if (text) {
39
+ try {
40
+ payload = JSON.parse(text);
41
+ } catch (parseError) {
42
+ throw new Error(`Invalid MCP response from ${serverName}: ${text.slice(0, 200)}`);
43
+ }
44
+ }
45
+
46
+ if (!response.ok) {
47
+ const errorMessage = payload?.error?.message || payload?.message || response.statusText;
48
+ throw new Error(`MCP request failed for ${serverName}: ${errorMessage}`);
49
+ }
50
+
51
+ if (payload?.error) {
52
+ throw new Error(payload.error.message || `MCP tool call failed for ${serverName}`);
53
+ }
54
+
55
+ return payload?.result ?? payload ?? null;
56
+ } finally {
57
+ clearTimeout(timeout);
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ callMcpTool
63
+ };
@@ -27,43 +27,23 @@ class AliyunDmsAdapter extends DatabaseManagerAdapter {
27
27
  }
28
28
 
29
29
  async listInstances(params) {
30
- return {
31
- mcpServer: this.mcpServer,
32
- tool: this.resolveTool('listInstances'),
33
- params: params || {}
34
- };
30
+ return this.callMcpTool('listInstances', params || {});
35
31
  }
36
32
 
37
33
  async executeScript(params) {
38
- return {
39
- mcpServer: this.mcpServer,
40
- tool: this.resolveTool('executeScript'),
41
- params
42
- };
34
+ return this.callMcpTool('executeScript', params);
43
35
  }
44
36
 
45
37
  async searchDatabase(searchKey) {
46
- return {
47
- mcpServer: this.mcpServer,
48
- tool: this.resolveTool('searchDatabase'),
49
- params: { search_key: searchKey }
50
- };
38
+ return this.callMcpTool('searchDatabase', { search_key: searchKey });
51
39
  }
52
40
 
53
41
  async listTables(databaseId) {
54
- return {
55
- mcpServer: this.mcpServer,
56
- tool: this.resolveTool('listTables'),
57
- params: { database_id: databaseId }
58
- };
42
+ return this.callMcpTool('listTables', { database_id: databaseId });
59
43
  }
60
44
 
61
45
  async getTableDetail(tableGuid) {
62
- return {
63
- mcpServer: this.mcpServer,
64
- tool: this.resolveTool('getTableDetailInfo'),
65
- params: { table_guid: tableGuid }
66
- };
46
+ return this.callMcpTool('getTableDetailInfo', { table_guid: tableGuid });
67
47
  }
68
48
 
69
49
  /**
@@ -73,11 +53,7 @@ class AliyunDmsAdapter extends DatabaseManagerAdapter {
73
53
  * @returns {Promise<object>}
74
54
  */
75
55
  async generateSql(databaseId, question) {
76
- return {
77
- mcpServer: this.mcpServer,
78
- tool: this.resolveTool('generateSql'),
79
- params: { database_id: databaseId, question }
80
- };
56
+ return this.callMcpTool('generateSql', { database_id: databaseId, question });
81
57
  }
82
58
 
83
59
  /**
@@ -87,11 +63,7 @@ class AliyunDmsAdapter extends DatabaseManagerAdapter {
87
63
  * @returns {Promise<object>}
88
64
  */
89
65
  async optimizeSql(databaseId, sql) {
90
- return {
91
- mcpServer: this.mcpServer,
92
- tool: this.resolveTool('optimizeSql'),
93
- params: { database_id: databaseId, sql }
94
- };
66
+ return this.callMcpTool('optimizeSql', { database_id: databaseId, sql });
95
67
  }
96
68
  }
97
69