flowmind 1.4.8 → 1.5.0
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 +21 -0
- package/bin/flowmind.js +219 -55
- package/core/adapters/workflow-adapter.js +9 -0
- package/core/config-manager.js +8 -1
- package/core/index.js +33 -17
- package/core/learning-engine.js +406 -0
- package/core/providers/friday/flow-adapter.js +8 -0
- package/core/scene-matcher.js +5 -1
- package/core/sdd-agent-sync.js +467 -0
- package/core/skill-loader.js +51 -10
- package/core/utils.js +55 -1
- package/dashboard/app.jsx +5 -5
- package/dashboard/components/ActivityFeed.jsx +7 -6
- package/dashboard/components/DragonPanel.jsx +4 -16
- package/dashboard/components/McpStatusBar.jsx +3 -2
- package/dashboard/components/StatsRow.jsx +6 -8
- package/package.json +2 -2
- package/skills/auto-flow/index.js +320 -45
- package/skills/data-logic-validation/index.js +9 -5
- package/skills/resource-bind/SKILL.md +21 -1
- package/skills/resource-bind/index.js +206 -14
- package/skills/yapi-sync-interface/index.js +14 -7
- package/skills/yuque-sync-design/index.js +15 -8
- package/tui/app.jsx +44 -28
- package/tui/components/ChatPanel.jsx +55 -43
- package/tui/components/DragonTotem.jsx +4 -73
- package/tui/components/Sidebar.jsx +7 -7
- package/tui/components/StatusBar.jsx +5 -6
- package/tui/format-result.js +164 -0
- package/tui/ui.js +186 -0
|
@@ -3,44 +3,74 @@
|
|
|
3
3
|
* Manage database, Redis, API, and other external resource connections
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const PROVIDER_MAP = {
|
|
7
|
+
'yapi-mcp': { componentType: 'apiDoc', provider: 'yapi', mcpServer: 'yapi-mcp' },
|
|
8
|
+
yapi: { componentType: 'apiDoc', provider: 'yapi', mcpServer: 'yapi-mcp' },
|
|
9
|
+
'yuque-mcp': { componentType: 'knowledgeBase', provider: 'yuque', mcpServer: 'yuque-mcp' },
|
|
10
|
+
yuque: { componentType: 'knowledgeBase', provider: 'yuque', mcpServer: 'yuque-mcp' },
|
|
11
|
+
'aliyun-dms-mcp-server': { componentType: 'databaseManager', provider: 'aliyun-dms', mcpServer: 'aliyun-dms-mcp-server' },
|
|
12
|
+
'aliyun-dms': { componentType: 'databaseManager', provider: 'aliyun-dms', mcpServer: 'aliyun-dms-mcp-server' },
|
|
13
|
+
'aliyun-redis': { componentType: 'redisMonitor', provider: 'aliyun-redis', mcpServer: 'aliyun-redis' },
|
|
14
|
+
redis: { componentType: 'redisMonitor', provider: 'aliyun-redis', mcpServer: 'aliyun-redis' },
|
|
15
|
+
'friday-auto-flow': { componentType: 'workflow', provider: 'friday-flow', mcpServer: 'friday-auto-flow' },
|
|
16
|
+
'friday-flow': { componentType: 'workflow', provider: 'friday-flow', mcpServer: 'friday-auto-flow' },
|
|
17
|
+
'aliyun-sls': { componentType: 'logService', provider: 'aliyun-sls', mcpServer: 'aliyun-sls' },
|
|
18
|
+
sls: { componentType: 'logService', provider: 'aliyun-sls', mcpServer: 'aliyun-sls' }
|
|
19
|
+
};
|
|
20
|
+
|
|
6
21
|
module.exports = {
|
|
7
|
-
canHandle(input
|
|
22
|
+
canHandle(input) {
|
|
8
23
|
if (!input) return false;
|
|
9
|
-
return /资源绑定|resource.*bind|数据库.*连接|redis.*连接|连接.*管理|connection.*manage
|
|
24
|
+
return /资源绑定|resource.*bind|数据库.*连接|redis.*连接|连接.*管理|connection.*manage|绑定.*(mcp|token|地址|endpoint|url|host)|使用.*已绑定/i.test(input);
|
|
10
25
|
},
|
|
11
26
|
|
|
12
27
|
async execute(input, context) {
|
|
13
28
|
const registry = context.componentRegistry;
|
|
14
29
|
const params = parseResourceParams(input);
|
|
15
30
|
|
|
31
|
+
if (params.action === 'bind') {
|
|
32
|
+
return bindResource(input, context, params);
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
if (params.action === 'list') {
|
|
17
36
|
const components = registry?.getAll ? registry.getAll() : [];
|
|
37
|
+
const bindings = await context.flowmind?.learning?.listResourceBindings?.() || [];
|
|
18
38
|
return {
|
|
19
39
|
type: 'result',
|
|
20
40
|
skill: 'resource-bind',
|
|
21
|
-
message: `Found ${components.length} configured component(s)`,
|
|
41
|
+
message: `Found ${components.length} configured component(s) and ${bindings.length} learned binding(s)`,
|
|
22
42
|
data: {
|
|
23
|
-
components: components.map(c => ({
|
|
43
|
+
components: components.map((c) => ({
|
|
24
44
|
name: c.name,
|
|
25
45
|
type: c.type,
|
|
26
46
|
provider: c.provider,
|
|
27
47
|
active: c.active,
|
|
28
48
|
initialized: c.initialized,
|
|
29
49
|
mcpServer: c.mcpServer
|
|
30
|
-
}))
|
|
50
|
+
})),
|
|
51
|
+
bindings
|
|
31
52
|
},
|
|
32
53
|
input,
|
|
33
54
|
timestamp: new Date().toISOString()
|
|
34
55
|
};
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
if (params.action === 'status') {
|
|
38
|
-
const status = registry ? registry.getStatus() : {};
|
|
58
|
+
if (params.action === 'status' || params.action === 'use' || params.action === 'show') {
|
|
59
|
+
const status = registry?.getStatus ? registry.getStatus() : {};
|
|
60
|
+
const matchedBinding = context.resourceBinding
|
|
61
|
+
|| await context.flowmind?.learning?.matchResourceBinding?.(input, 'resource-bind')
|
|
62
|
+
|| null;
|
|
63
|
+
|
|
39
64
|
return {
|
|
40
65
|
type: 'result',
|
|
41
66
|
skill: 'resource-bind',
|
|
42
|
-
message:
|
|
43
|
-
|
|
67
|
+
message: matchedBinding
|
|
68
|
+
? `Using learned binding for business: ${matchedBinding.business}`
|
|
69
|
+
: 'Resource connection status',
|
|
70
|
+
data: {
|
|
71
|
+
status,
|
|
72
|
+
matchedBinding
|
|
73
|
+
},
|
|
44
74
|
input,
|
|
45
75
|
timestamp: new Date().toISOString()
|
|
46
76
|
};
|
|
@@ -49,10 +79,15 @@ module.exports = {
|
|
|
49
79
|
return {
|
|
50
80
|
type: 'result',
|
|
51
81
|
skill: 'resource-bind',
|
|
52
|
-
message: 'Resource binding. Available actions: list, status',
|
|
82
|
+
message: 'Resource binding. Available actions: bind, list, status, use',
|
|
53
83
|
data: {
|
|
54
|
-
actions: [
|
|
55
|
-
|
|
84
|
+
actions: [
|
|
85
|
+
'bind - Save MCP/address/token for a business',
|
|
86
|
+
'list - List configured resources and learned bindings',
|
|
87
|
+
'status - Show connection status and matched binding',
|
|
88
|
+
'use - Resolve a learned binding for the current business'
|
|
89
|
+
],
|
|
90
|
+
supportedTypes: ['MySQL', 'PostgreSQL', 'Redis', 'REST API', 'YApi', 'Yuque', 'Workflow']
|
|
56
91
|
},
|
|
57
92
|
input,
|
|
58
93
|
timestamp: new Date().toISOString()
|
|
@@ -60,10 +95,167 @@ module.exports = {
|
|
|
60
95
|
}
|
|
61
96
|
};
|
|
62
97
|
|
|
98
|
+
async function bindResource(input, context, params) {
|
|
99
|
+
const learning = context.flowmind?.learning;
|
|
100
|
+
if (!learning?.saveResourceBinding) {
|
|
101
|
+
return {
|
|
102
|
+
type: 'error',
|
|
103
|
+
skill: 'resource-bind',
|
|
104
|
+
message: 'Learning engine is not available for resource binding',
|
|
105
|
+
input,
|
|
106
|
+
timestamp: new Date().toISOString()
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const binding = buildBinding(input, params, context);
|
|
111
|
+
const saved = await learning.saveResourceBinding(binding, {
|
|
112
|
+
currentSkill: 'resource-bind'
|
|
113
|
+
});
|
|
114
|
+
const storagePath = learning.expandPath(learning.learningPath);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
type: 'result',
|
|
118
|
+
skill: 'resource-bind',
|
|
119
|
+
message: `Saved learned binding for business: ${saved.business}`,
|
|
120
|
+
data: {
|
|
121
|
+
binding: saved,
|
|
122
|
+
storage: `${storagePath}/resource-bindings.json`
|
|
123
|
+
},
|
|
124
|
+
input,
|
|
125
|
+
timestamp: new Date().toISOString()
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildBinding(input, params, context) {
|
|
130
|
+
const inferred = resolveProviderInfo(params);
|
|
131
|
+
const business = inferBusiness(input, params, inferred);
|
|
132
|
+
const aliases = [business, params.project, params.namespace, params.alias, params.env].filter(Boolean);
|
|
133
|
+
const keywords = extractKeywords(input, aliases);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
business,
|
|
137
|
+
aliases,
|
|
138
|
+
componentType: params.componentType || inferred.componentType || context.flowmind?.learning?.inferComponentType?.(input, 'resource-bind') || null,
|
|
139
|
+
provider: params.provider || inferred.provider || null,
|
|
140
|
+
mcpServer: params.mcpServer || inferred.mcpServer || null,
|
|
141
|
+
connection: {
|
|
142
|
+
address: params.address || params.url || params.endpoint || null,
|
|
143
|
+
host: params.host || null,
|
|
144
|
+
port: params.port || null,
|
|
145
|
+
token: params.token || null,
|
|
146
|
+
apiKey: params.apiKey || null,
|
|
147
|
+
namespace: params.namespace || null,
|
|
148
|
+
project: params.project || null,
|
|
149
|
+
sourceId: params.sourceId || null,
|
|
150
|
+
databaseId: params.databaseId || null,
|
|
151
|
+
env: params.env || null
|
|
152
|
+
},
|
|
153
|
+
metadata: {
|
|
154
|
+
input
|
|
155
|
+
},
|
|
156
|
+
keywords
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
63
160
|
function parseResourceParams(input) {
|
|
64
161
|
const params = {};
|
|
65
|
-
if (/列表|list
|
|
162
|
+
if (/列表|list|查看全部|show all/i.test(input)) params.action = 'list';
|
|
66
163
|
if (/状态|status|连接/i.test(input)) params.action = 'status';
|
|
67
|
-
if (/绑定|bind|添加|add
|
|
164
|
+
if (/绑定|bind|添加|add|记住|保存/i.test(input)) params.action = 'bind';
|
|
165
|
+
if (/使用|use|套用|复用/i.test(input)) params.action = 'use';
|
|
166
|
+
if (/查看绑定|show binding|显示绑定/i.test(input)) params.action = 'show';
|
|
167
|
+
|
|
168
|
+
const keyMap = {
|
|
169
|
+
business: ['业务', 'business', 'biz', 'service'],
|
|
170
|
+
alias: ['别名', 'alias'],
|
|
171
|
+
mcpServer: ['mcp', 'mcpServer', 'mcp_server'],
|
|
172
|
+
provider: ['provider', '服务商'],
|
|
173
|
+
componentType: ['component', 'componentType', '组件'],
|
|
174
|
+
address: ['地址', 'address'],
|
|
175
|
+
url: ['url', 'baseUrl', 'baseURL'],
|
|
176
|
+
endpoint: ['endpoint'],
|
|
177
|
+
host: ['host'],
|
|
178
|
+
port: ['port'],
|
|
179
|
+
token: ['token'],
|
|
180
|
+
apiKey: ['apiKey', 'apikey', 'key'],
|
|
181
|
+
namespace: ['namespace', 'repo', '仓库'],
|
|
182
|
+
project: ['project', '项目'],
|
|
183
|
+
sourceId: ['sourceId', 'source_id'],
|
|
184
|
+
databaseId: ['databaseId', 'database_id'],
|
|
185
|
+
env: ['env', '环境']
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (const [targetKey, aliases] of Object.entries(keyMap)) {
|
|
189
|
+
const value = findKeyValue(input, aliases);
|
|
190
|
+
if (value) params[targetKey] = value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const bizSuffixMatch = input.match(/(?:绑定|bind)?\s*([\u4e00-\u9fa5A-Za-z0-9._-]+)\s*业务/i);
|
|
194
|
+
if (!params.business && bizSuffixMatch) {
|
|
195
|
+
params.business = bizSuffixMatch[1];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!params.business) {
|
|
199
|
+
const bizKeywordMatch = input.match(/(?:绑定|bind)\s*([\u4e00-\u9fa5A-Za-z0-9._-]+?)\s*(平台|服务|环境|工程|项目)/i);
|
|
200
|
+
if (bizKeywordMatch) {
|
|
201
|
+
params.business = `${bizKeywordMatch[1]}${bizKeywordMatch[2]}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const urlMatch = input.match(/https?:\/\/[^\s,,]+/i);
|
|
206
|
+
if (!params.address && !params.url && urlMatch) {
|
|
207
|
+
params.address = urlMatch[0];
|
|
208
|
+
}
|
|
209
|
+
|
|
68
210
|
return params;
|
|
69
211
|
}
|
|
212
|
+
|
|
213
|
+
function findKeyValue(input, aliases) {
|
|
214
|
+
for (const alias of aliases) {
|
|
215
|
+
const regex = new RegExp(`${escapeRegex(alias)}\\s*[:=:]\\s*([^\\s,,]+)`, 'i');
|
|
216
|
+
const match = input.match(regex);
|
|
217
|
+
if (match) return match[1].trim();
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveProviderInfo(params) {
|
|
223
|
+
const candidates = [params.provider, params.mcpServer, params.componentType]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.map((value) => String(value).toLowerCase());
|
|
226
|
+
|
|
227
|
+
for (const candidate of candidates) {
|
|
228
|
+
if (PROVIDER_MAP[candidate]) return PROVIDER_MAP[candidate];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function inferBusiness(input, params, inferred = {}) {
|
|
235
|
+
const explicit = params.business || params.project || params.namespace;
|
|
236
|
+
if (explicit) return explicit;
|
|
237
|
+
|
|
238
|
+
const keywordMatch = String(input || '').match(/(?:绑定|bind)\s*([\u4e00-\u9fa5A-Za-z0-9._-]+?)\s*(平台|服务|环境|工程|项目)/i);
|
|
239
|
+
if (keywordMatch) {
|
|
240
|
+
return `${keywordMatch[1]}${keywordMatch[2]}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (inferred.componentType === 'workflow') {
|
|
244
|
+
return params.env ? `发布平台-${params.env}` : '发布平台';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (inferred.provider) {
|
|
248
|
+
return params.env ? `${inferred.provider}-${params.env}` : inferred.provider;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return params.env ? `default-${params.env}` : 'default';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function extractKeywords(input, aliases = []) {
|
|
255
|
+
const words = input.match(/[\u4e00-\u9fa5A-Za-z0-9._-]+/g) || [];
|
|
256
|
+
return [...new Set([...aliases.filter(Boolean), ...words])].slice(0, 16);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function escapeRegex(value) {
|
|
260
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
261
|
+
}
|
|
@@ -11,8 +11,11 @@ module.exports = {
|
|
|
11
11
|
|
|
12
12
|
async execute(input, context) {
|
|
13
13
|
const apiDoc = context.componentRegistry?.getAdapter('apiDoc');
|
|
14
|
+
const learnedBinding = context.resourceBinding?.componentType === 'apiDoc'
|
|
15
|
+
? context.resourceBinding
|
|
16
|
+
: null;
|
|
14
17
|
|
|
15
|
-
if (!apiDoc) {
|
|
18
|
+
if (!apiDoc && !learnedBinding) {
|
|
16
19
|
return {
|
|
17
20
|
type: 'result',
|
|
18
21
|
skill: 'yapi-sync-interface',
|
|
@@ -32,7 +35,8 @@ module.exports = {
|
|
|
32
35
|
message: `Searching YApi for: ${params.keyword || 'all'}`,
|
|
33
36
|
data: {
|
|
34
37
|
mcpTool: 'yapi_search_apis',
|
|
35
|
-
args: { projectKeyword: params.project, nameKeyword: params.keyword, limit: 20 }
|
|
38
|
+
args: { projectKeyword: params.project || learnedBinding?.connection?.project, nameKeyword: params.keyword, limit: 20 },
|
|
39
|
+
binding: learnedBinding
|
|
36
40
|
},
|
|
37
41
|
input,
|
|
38
42
|
timestamp: new Date().toISOString()
|
|
@@ -43,10 +47,11 @@ module.exports = {
|
|
|
43
47
|
return {
|
|
44
48
|
type: 'result',
|
|
45
49
|
skill: 'yapi-sync-interface',
|
|
46
|
-
message: `Listing YApi categories for project: ${params.project || 'default'}`,
|
|
50
|
+
message: `Listing YApi categories for project: ${params.project || learnedBinding?.connection?.project || 'default'}`,
|
|
47
51
|
data: {
|
|
48
52
|
mcpTool: 'yapi_get_categories',
|
|
49
|
-
args: { projectId: params.project }
|
|
53
|
+
args: { projectId: params.project || learnedBinding?.connection?.project },
|
|
54
|
+
binding: learnedBinding
|
|
50
55
|
},
|
|
51
56
|
input,
|
|
52
57
|
timestamp: new Date().toISOString()
|
|
@@ -57,10 +62,11 @@ module.exports = {
|
|
|
57
62
|
return {
|
|
58
63
|
type: 'result',
|
|
59
64
|
skill: 'yapi-sync-interface',
|
|
60
|
-
message: `Exporting YApi project: ${params.project}`,
|
|
65
|
+
message: `Exporting YApi project: ${params.project || learnedBinding?.connection?.project || 'default'}`,
|
|
61
66
|
data: {
|
|
62
67
|
mcpTool: 'yapi_export_project',
|
|
63
|
-
args: { projectId: params.project, type: params.format || 'swagger' }
|
|
68
|
+
args: { projectId: params.project || learnedBinding?.connection?.project, type: params.format || 'swagger' },
|
|
69
|
+
binding: learnedBinding
|
|
64
70
|
},
|
|
65
71
|
input,
|
|
66
72
|
timestamp: new Date().toISOString()
|
|
@@ -73,7 +79,8 @@ module.exports = {
|
|
|
73
79
|
message: 'YApi sync. Available actions: search, list, export, import',
|
|
74
80
|
data: {
|
|
75
81
|
actions: ['search - Search interfaces', 'list - List categories', 'export - Export project', 'import - Import Swagger'],
|
|
76
|
-
mcpTools: ['yapi_search_apis', 'yapi_get_categories', 'yapi_save_api', 'yapi_import_swagger', 'yapi_export_project']
|
|
82
|
+
mcpTools: ['yapi_search_apis', 'yapi_get_categories', 'yapi_save_api', 'yapi_import_swagger', 'yapi_export_project'],
|
|
83
|
+
binding: learnedBinding
|
|
77
84
|
},
|
|
78
85
|
input,
|
|
79
86
|
timestamp: new Date().toISOString()
|
|
@@ -14,8 +14,11 @@ module.exports = {
|
|
|
14
14
|
|
|
15
15
|
async execute(input, context) {
|
|
16
16
|
const knowledgeBase = context.componentRegistry?.getAdapter('knowledgeBase');
|
|
17
|
+
const learnedBinding = context.resourceBinding?.componentType === 'knowledgeBase'
|
|
18
|
+
? context.resourceBinding
|
|
19
|
+
: null;
|
|
17
20
|
|
|
18
|
-
if (!knowledgeBase) {
|
|
21
|
+
if (!knowledgeBase && !learnedBinding) {
|
|
19
22
|
return {
|
|
20
23
|
type: 'result',
|
|
21
24
|
skill: 'yuque-sync-design',
|
|
@@ -35,7 +38,8 @@ module.exports = {
|
|
|
35
38
|
message: `Searching Yuque for: ${params.keyword || 'all'}`,
|
|
36
39
|
data: {
|
|
37
40
|
mcpTool: 'search',
|
|
38
|
-
args: { q: params.keyword, type: 'doc' }
|
|
41
|
+
args: { q: params.keyword, type: 'doc' },
|
|
42
|
+
binding: learnedBinding
|
|
39
43
|
},
|
|
40
44
|
input,
|
|
41
45
|
timestamp: new Date().toISOString()
|
|
@@ -49,7 +53,8 @@ module.exports = {
|
|
|
49
53
|
message: 'Listing Yuque repositories',
|
|
50
54
|
data: {
|
|
51
55
|
mcpTool: 'get_user_repos',
|
|
52
|
-
args: {}
|
|
56
|
+
args: {},
|
|
57
|
+
binding: learnedBinding
|
|
53
58
|
},
|
|
54
59
|
input,
|
|
55
60
|
timestamp: new Date().toISOString()
|
|
@@ -57,7 +62,7 @@ module.exports = {
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
if (params.action === 'sync') {
|
|
60
|
-
return syncDesignDoc(params, input);
|
|
65
|
+
return syncDesignDoc(params, input, learnedBinding);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
return {
|
|
@@ -66,7 +71,8 @@ module.exports = {
|
|
|
66
71
|
message: 'Yuque design sync. Available actions: search, list, sync',
|
|
67
72
|
data: {
|
|
68
73
|
actions: ['search - Search documents', 'list - List repos', 'sync - Sync design doc'],
|
|
69
|
-
mcpTools: ['get_user_repos', 'get_repo_docs', 'get_doc', 'create_doc', 'update_doc', 'search']
|
|
74
|
+
mcpTools: ['get_user_repos', 'get_repo_docs', 'get_doc', 'create_doc', 'update_doc', 'search'],
|
|
75
|
+
binding: learnedBinding
|
|
70
76
|
},
|
|
71
77
|
input,
|
|
72
78
|
timestamp: new Date().toISOString()
|
|
@@ -92,7 +98,7 @@ function parseYuqueParams(input) {
|
|
|
92
98
|
return params;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
async function syncDesignDoc(params, input) {
|
|
101
|
+
async function syncDesignDoc(params, input, learnedBinding) {
|
|
96
102
|
const filePath = params.path || 'DESIGN.md';
|
|
97
103
|
|
|
98
104
|
if (!(await fs.pathExists(filePath))) {
|
|
@@ -115,12 +121,13 @@ async function syncDesignDoc(params, input) {
|
|
|
115
121
|
data: {
|
|
116
122
|
mcpTool: 'create_doc',
|
|
117
123
|
args: {
|
|
118
|
-
namespace: params.repo,
|
|
124
|
+
namespace: params.repo || learnedBinding?.connection?.namespace,
|
|
119
125
|
slug: title.toLowerCase().replace(/\s+/g, '-'),
|
|
120
126
|
title,
|
|
121
127
|
body: content,
|
|
122
128
|
format: 'markdown'
|
|
123
|
-
}
|
|
129
|
+
},
|
|
130
|
+
binding: learnedBinding
|
|
124
131
|
},
|
|
125
132
|
input,
|
|
126
133
|
timestamp: new Date().toISOString()
|
package/tui/app.jsx
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
|
-
const { Box,
|
|
2
|
+
const { Box, useApp, useInput } = require('ink');
|
|
3
3
|
const Sidebar = require('./components/Sidebar.jsx');
|
|
4
4
|
const ChatPanel = require('./components/ChatPanel.jsx');
|
|
5
|
-
const ResultPanel = require('./components/ResultPanel.jsx');
|
|
6
5
|
const StatusBar = require('./components/StatusBar.jsx');
|
|
6
|
+
const { formatResultText } = require('./format-result');
|
|
7
7
|
|
|
8
|
-
function App({ flowmind }) {
|
|
9
|
-
const [
|
|
8
|
+
function App({ flowmind, asciiMode = false }) {
|
|
9
|
+
const [messages, setMessages] = React.useState([]);
|
|
10
10
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
11
11
|
const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
|
|
12
12
|
const mountedRef = React.useRef(true);
|
|
13
|
+
const messageIdRef = React.useRef(0);
|
|
13
14
|
const { exit } = useApp();
|
|
14
15
|
|
|
15
16
|
React.useEffect(() => {
|
|
16
17
|
return () => { mountedRef.current = false; };
|
|
17
18
|
}, []);
|
|
18
19
|
|
|
20
|
+
const appendMessage = React.useCallback((role, text, metadata = {}) => {
|
|
21
|
+
const id = ++messageIdRef.current;
|
|
22
|
+
setMessages(prev => [...prev, { id, role, text, metadata }]);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
19
25
|
// Ctrl+C always exits; Tab switches focus between panels
|
|
20
26
|
useInput((input, key) => {
|
|
21
27
|
if (key.ctrl && input === 'c') exit();
|
|
@@ -24,51 +30,61 @@ function App({ flowmind }) {
|
|
|
24
30
|
}
|
|
25
31
|
});
|
|
26
32
|
|
|
27
|
-
const handleCommand = React.useCallback(async (input
|
|
33
|
+
const handleCommand = React.useCallback(async (input) => {
|
|
28
34
|
if (!mountedRef.current) return;
|
|
35
|
+
appendMessage('user', input);
|
|
29
36
|
setIsProcessing(true);
|
|
37
|
+
|
|
30
38
|
try {
|
|
31
39
|
const result = await flowmind.process(input);
|
|
32
40
|
if (!mountedRef.current) return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
setResults(prev => [...prev, result]);
|
|
41
|
+
|
|
42
|
+
appendMessage('flowmind', formatResultText(result), {
|
|
43
|
+
type: result.type,
|
|
44
|
+
skill: result.metadata?.skill || result.skill,
|
|
45
|
+
duration: result.metadata?.duration,
|
|
46
|
+
scene: result.metadata?.sceneMatch?.scene?.name
|
|
47
|
+
});
|
|
42
48
|
} catch (e) {
|
|
43
|
-
if (mountedRef.current)
|
|
49
|
+
if (mountedRef.current) appendMessage('flowmind', 'Error: ' + e.message, { type: 'error' });
|
|
44
50
|
} finally {
|
|
45
51
|
if (mountedRef.current) setIsProcessing(false);
|
|
46
52
|
}
|
|
47
|
-
}, [flowmind]);
|
|
53
|
+
}, [appendMessage, flowmind]);
|
|
48
54
|
|
|
49
55
|
const handleSkillSelect = React.useCallback((skill) => {
|
|
50
56
|
if (!mountedRef.current) return;
|
|
51
57
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
appendMessage(
|
|
59
|
+
'system',
|
|
60
|
+
[
|
|
61
|
+
`Skill: ${skill.name}`,
|
|
62
|
+
`Category: ${skill.category || 'general'}`,
|
|
63
|
+
`Description: ${skill.definition?.description || 'No description'}`,
|
|
64
|
+
`Path: ${skill.path}`
|
|
65
|
+
].join('\n'),
|
|
66
|
+
{ label: 'skill inspect' }
|
|
67
|
+
);
|
|
68
|
+
setFocusPanel('chat');
|
|
57
69
|
} catch (e) {
|
|
58
70
|
// ignore skill select errors
|
|
59
71
|
}
|
|
60
|
-
}, []);
|
|
72
|
+
}, [appendMessage]);
|
|
61
73
|
|
|
62
74
|
return (
|
|
63
75
|
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
64
76
|
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
65
|
-
React.createElement(Sidebar, { flowmind: flowmind, width:
|
|
66
|
-
React.createElement(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
React.createElement(Sidebar, { flowmind: flowmind, width: 32, onSkillSelect: handleSkillSelect, focused: focusPanel === 'sidebar', asciiMode: asciiMode }),
|
|
78
|
+
React.createElement(ChatPanel, {
|
|
79
|
+
messages,
|
|
80
|
+
onSubmit: handleCommand,
|
|
81
|
+
isProcessing: isProcessing,
|
|
82
|
+
onExit: exit,
|
|
83
|
+
focused: focusPanel === 'chat',
|
|
84
|
+
asciiMode: asciiMode
|
|
85
|
+
})
|
|
70
86
|
),
|
|
71
|
-
React.createElement(StatusBar, { flowmind: flowmind, focusPanel: focusPanel })
|
|
87
|
+
React.createElement(StatusBar, { flowmind: flowmind, focusPanel: focusPanel, asciiMode: asciiMode })
|
|
72
88
|
)
|
|
73
89
|
);
|
|
74
90
|
}
|
|
@@ -2,18 +2,13 @@ const React = require('react');
|
|
|
2
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
|
+
const { getBorderStyle, isExitCommand } = require('../ui');
|
|
5
6
|
|
|
6
|
-
function ChatPanel({ onSubmit, isProcessing, onExit, focused }) {
|
|
7
|
+
function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMode = false }) {
|
|
7
8
|
const [input, setInput] = React.useState('');
|
|
8
|
-
const [history, setHistory] = React.useState([]);
|
|
9
9
|
const [cmdHistory, setCmdHistory] = React.useState([]);
|
|
10
10
|
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
11
11
|
const [savedInput, setSavedInput] = React.useState('');
|
|
12
|
-
const mountedRef = React.useRef(true);
|
|
13
|
-
|
|
14
|
-
React.useEffect(() => {
|
|
15
|
-
return () => { mountedRef.current = false; };
|
|
16
|
-
}, []);
|
|
17
12
|
|
|
18
13
|
// Handle Up/Down arrow for command history (only when focused)
|
|
19
14
|
useInput((ch, key) => {
|
|
@@ -40,58 +35,75 @@ function ChatPanel({ onSubmit, isProcessing, onExit, focused }) {
|
|
|
40
35
|
});
|
|
41
36
|
|
|
42
37
|
const handleSubmit = (value) => {
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
const normalized = value.trim();
|
|
39
|
+
if (!normalized) return;
|
|
45
40
|
// Add to command history (deduplicate consecutive)
|
|
46
|
-
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !==
|
|
47
|
-
setCmdHistory(prev => [...prev,
|
|
41
|
+
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== normalized) {
|
|
42
|
+
setCmdHistory(prev => [...prev, normalized]);
|
|
48
43
|
}
|
|
49
44
|
setHistoryIndex(-1);
|
|
50
45
|
setSavedInput('');
|
|
51
46
|
setInput('');
|
|
52
|
-
if (
|
|
47
|
+
if (isExitCommand(normalized)) {
|
|
53
48
|
if (onExit) onExit();
|
|
54
49
|
return;
|
|
55
50
|
}
|
|
56
|
-
|
|
57
|
-
onSubmit(value, (response) => {
|
|
58
|
-
if (mountedRef.current) {
|
|
59
|
-
setHistory(prev => [...prev, { role: 'flowmind', text: response }]);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
} catch (e) {
|
|
63
|
-
if (mountedRef.current) {
|
|
64
|
-
setHistory(prev => [...prev, { role: 'flowmind', text: 'Error: ' + e.message }]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
51
|
+
onSubmit(normalized);
|
|
67
52
|
};
|
|
68
53
|
|
|
69
|
-
const
|
|
54
|
+
const displayMessages = messages.slice(-18);
|
|
55
|
+
|
|
56
|
+
const renderMeta = (message) => {
|
|
57
|
+
const metaParts = [];
|
|
58
|
+
|
|
59
|
+
if (message.metadata?.label) metaParts.push(message.metadata.label);
|
|
60
|
+
if (message.metadata?.skill) metaParts.push(message.metadata.skill);
|
|
61
|
+
if (message.metadata?.duration) metaParts.push(message.metadata.duration + 'ms');
|
|
62
|
+
if (message.metadata?.scene) metaParts.push('scene: ' + message.metadata.scene);
|
|
63
|
+
|
|
64
|
+
if (metaParts.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
return React.createElement(
|
|
67
|
+
Text,
|
|
68
|
+
{ color: 'gray' },
|
|
69
|
+
metaParts.join(' | ')
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const renderMessage = (message) => {
|
|
74
|
+
const isUser = message.role === 'user';
|
|
75
|
+
const isSystem = message.role === 'system';
|
|
76
|
+
const accentColor = isUser ? 'green' : (isSystem ? 'yellow' : 'cyan');
|
|
77
|
+
const bodyColor = isUser ? 'white' : (isSystem ? 'yellow' : 'cyan');
|
|
78
|
+
const label = isUser ? 'You' : (isSystem ? 'FlowMind Guide' : 'FlowMind');
|
|
79
|
+
|
|
80
|
+
return React.createElement(
|
|
81
|
+
Box,
|
|
82
|
+
{ key: message.id, flexDirection: 'column', marginBottom: 1 },
|
|
83
|
+
React.createElement(Text, { color: accentColor, bold: true }, label),
|
|
84
|
+
renderMeta(message),
|
|
85
|
+
React.createElement(Text, { color: bodyColor }, message.text)
|
|
86
|
+
);
|
|
87
|
+
};
|
|
70
88
|
|
|
71
89
|
return (
|
|
72
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
73
|
-
React.createElement(Text, { bold: true, color: focused ? 'green' : 'gray' }, focused ? '
|
|
74
|
-
React.createElement(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
React.createElement(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
: React.createElement(Text, null,
|
|
84
|
-
React.createElement(Text, { color: 'cyan', bold: true }, '< '),
|
|
85
|
-
React.createElement(Text, { color: 'cyan' }, msg.text)
|
|
86
|
-
)
|
|
87
|
-
)
|
|
88
|
-
)
|
|
90
|
+
React.createElement(Box, { flexDirection: 'column', flexGrow: 1, borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'green' : 'gray', paddingX: 1 },
|
|
91
|
+
React.createElement(Text, { bold: true, color: focused ? 'green' : 'gray' }, focused ? 'FlowMind Chat [Focused]' : 'FlowMind Chat'),
|
|
92
|
+
React.createElement(Text, { color: 'gray' }, 'Single-pane conversation. Output stays in the same view.'),
|
|
93
|
+
React.createElement(Box, { flexDirection: 'column', flexGrow: 1, marginTop: 1, overflow: 'hidden' },
|
|
94
|
+
displayMessages.length === 0 && React.createElement(Box, { flexDirection: 'column' },
|
|
95
|
+
React.createElement(Text, { color: 'gray' }, 'Describe what you want FlowMind to do.'),
|
|
96
|
+
React.createElement(Text, { color: 'gray' }, 'The dialog history stays in this panel, with the input fixed at the bottom.'),
|
|
97
|
+
React.createElement(Text, { color: 'gray' }, 'Type `exit` or `/exit` to quit, or press Ctrl+C.')
|
|
98
|
+
),
|
|
99
|
+
displayMessages.map(renderMessage)
|
|
89
100
|
),
|
|
90
|
-
React.createElement(Box, { marginTop: 1 },
|
|
101
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'green' : 'gray', paddingX: 1, marginTop: 1 },
|
|
102
|
+
React.createElement(Text, { color: 'gray' }, 'Enter send Up/Down history Tab sidebar exit /exit :q Ctrl+C'),
|
|
91
103
|
isProcessing
|
|
92
104
|
? React.createElement(Text, { color: 'yellow' },
|
|
93
105
|
React.createElement(Spinner, { type: 'dots' }),
|
|
94
|
-
'
|
|
106
|
+
' FlowMind is working...'
|
|
95
107
|
)
|
|
96
108
|
: React.createElement(Box, null,
|
|
97
109
|
React.createElement(Text, { color: focused ? 'green' : 'gray', bold: true }, '> '),
|