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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MCP_SERVER_ALIASES = Object.freeze({
|
|
6
|
+
'aomi-yapi-mcp': 'yapi-mcp',
|
|
7
|
+
'aomi-yuque-mcp': 'yuque-mcp'
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const RESOURCE_BINDING_SPECS = Object.freeze({
|
|
11
|
+
yapi: { componentType: 'apiDoc', provider: 'yapi' },
|
|
12
|
+
yuque: { componentType: 'knowledgeBase', provider: 'yuque' },
|
|
13
|
+
database: { componentType: 'databaseQuery', provider: 'aliyun-rds-query' },
|
|
14
|
+
redis: { componentType: 'redisMonitor', provider: 'aliyun-redis' },
|
|
15
|
+
sls: { componentType: 'logService', provider: 'aliyun-sls' },
|
|
16
|
+
workflow: { componentType: 'workflow', provider: 'friday-flow' },
|
|
17
|
+
report: { componentType: 'report', provider: 'friday-report' }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function getHomeDir() {
|
|
21
|
+
return process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeMcpServer(name) {
|
|
25
|
+
if (!name) return null;
|
|
26
|
+
return MCP_SERVER_ALIASES[name] || name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deepMerge(target, source) {
|
|
30
|
+
if (!isObject(target) || !isObject(source)) {
|
|
31
|
+
return source;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = { ...target };
|
|
35
|
+
for (const [key, value] of Object.entries(source)) {
|
|
36
|
+
if (isObject(value) && isObject(result[key])) {
|
|
37
|
+
result[key] = deepMerge(result[key], value);
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isObject(value) {
|
|
46
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeResourceConfig(sddConfig) {
|
|
50
|
+
return {
|
|
51
|
+
version: sddConfig.version || '1.0',
|
|
52
|
+
lastUpdated: new Date().toISOString(),
|
|
53
|
+
resources: sddConfig.resources || {},
|
|
54
|
+
mcpServers: sddConfig.mcpServers || {},
|
|
55
|
+
aliases: sddConfig.aliases || {},
|
|
56
|
+
metadata: {
|
|
57
|
+
source: 'sdd-agent',
|
|
58
|
+
syncedAt: new Date().toISOString()
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildAliasIndex(aliasConfig = {}) {
|
|
64
|
+
const index = {};
|
|
65
|
+
for (const [alias, mapping] of Object.entries(aliasConfig)) {
|
|
66
|
+
if (!isObject(mapping)) continue;
|
|
67
|
+
for (const [resourceType, bindingKey] of Object.entries(mapping)) {
|
|
68
|
+
if (!bindingKey) continue;
|
|
69
|
+
const composite = `${resourceType}:${bindingKey}`;
|
|
70
|
+
index[composite] = index[composite] || [];
|
|
71
|
+
index[composite].push(alias);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return index;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inferBindingMcpServer(resourceType, resourceConfig = {}) {
|
|
78
|
+
if (resourceConfig.mcpServer) {
|
|
79
|
+
return normalizeMcpServer(resourceConfig.mcpServer);
|
|
80
|
+
}
|
|
81
|
+
if (isObject(resourceConfig.mcpServers)) {
|
|
82
|
+
let preferred = null;
|
|
83
|
+
if (resourceType === 'database') {
|
|
84
|
+
preferred = resourceConfig.mcpServers.directQuery || resourceConfig.mcpServers.dms;
|
|
85
|
+
} else if (resourceType === 'redis') {
|
|
86
|
+
preferred = resourceConfig.mcpServers.monitor || resourceConfig.mcpServers.directQuery;
|
|
87
|
+
} else {
|
|
88
|
+
preferred = resourceConfig.mcpServers.directQuery
|
|
89
|
+
|| resourceConfig.mcpServers.monitor
|
|
90
|
+
|| resourceConfig.mcpServers.dms;
|
|
91
|
+
}
|
|
92
|
+
return normalizeMcpServer(preferred);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildResourceBinding(resourceType, bindingKey, bindingValue, resourceConfig, aliasIndex) {
|
|
98
|
+
const spec = RESOURCE_BINDING_SPECS[resourceType];
|
|
99
|
+
if (!spec) return null;
|
|
100
|
+
|
|
101
|
+
const bindingAliases = aliasIndex[`${resourceType}:${bindingKey}`] || [];
|
|
102
|
+
const business = bindingAliases[0] || bindingValue.name || bindingKey;
|
|
103
|
+
const aliases = [
|
|
104
|
+
bindingKey,
|
|
105
|
+
bindingValue.name,
|
|
106
|
+
bindingValue.database,
|
|
107
|
+
bindingValue.namespace,
|
|
108
|
+
bindingValue.projectId,
|
|
109
|
+
bindingValue.envType,
|
|
110
|
+
bindingValue.env,
|
|
111
|
+
...bindingAliases
|
|
112
|
+
].filter(Boolean);
|
|
113
|
+
|
|
114
|
+
const env = bindingValue.envType || bindingValue.env || null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: `sdd-${resourceType}-${bindingKey}`,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
business,
|
|
120
|
+
aliases: [...new Set(aliases)],
|
|
121
|
+
componentType: spec.componentType,
|
|
122
|
+
provider: spec.provider,
|
|
123
|
+
mcpServer: inferBindingMcpServer(resourceType, resourceConfig),
|
|
124
|
+
source: 'sdd-agent-sync',
|
|
125
|
+
keywords: [...new Set([business, bindingKey, resourceType, env, ...aliases].filter(Boolean))],
|
|
126
|
+
connection: {
|
|
127
|
+
...bindingValue,
|
|
128
|
+
env
|
|
129
|
+
},
|
|
130
|
+
metadata: {
|
|
131
|
+
source: 'sdd-agent',
|
|
132
|
+
resourceType,
|
|
133
|
+
bindingKey
|
|
134
|
+
},
|
|
135
|
+
stats: {
|
|
136
|
+
useCount: 0,
|
|
137
|
+
lastUsed: null
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildLogBindings(resourceConfig = {}) {
|
|
143
|
+
const spec = RESOURCE_BINDING_SPECS.sls;
|
|
144
|
+
const envConfig = resourceConfig.envConfig || {};
|
|
145
|
+
return Object.entries(envConfig).map(([env, value]) => ({
|
|
146
|
+
id: `sdd-sls-${env}`,
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
business: `日志-${env}`,
|
|
149
|
+
aliases: [env, value.name, `${env}-日志`, `${env}-sls`].filter(Boolean),
|
|
150
|
+
componentType: spec.componentType,
|
|
151
|
+
provider: spec.provider,
|
|
152
|
+
mcpServer: normalizeMcpServer(resourceConfig.mcpServer),
|
|
153
|
+
source: 'sdd-agent-sync',
|
|
154
|
+
keywords: ['日志', 'log', 'trace', 'sls', env, value.logstore, value.project].filter(Boolean),
|
|
155
|
+
connection: {
|
|
156
|
+
env,
|
|
157
|
+
project: value.project,
|
|
158
|
+
endpoint: value.endpoint,
|
|
159
|
+
logstore: value.logstore,
|
|
160
|
+
...value
|
|
161
|
+
},
|
|
162
|
+
metadata: {
|
|
163
|
+
source: 'sdd-agent',
|
|
164
|
+
resourceType: 'sls',
|
|
165
|
+
env
|
|
166
|
+
},
|
|
167
|
+
stats: {
|
|
168
|
+
useCount: 0,
|
|
169
|
+
lastUsed: null
|
|
170
|
+
}
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function convertResourceBindings(sddConfig) {
|
|
175
|
+
const resources = sddConfig.resources || {};
|
|
176
|
+
const aliasIndex = buildAliasIndex(sddConfig.aliases);
|
|
177
|
+
const bindings = [];
|
|
178
|
+
|
|
179
|
+
for (const [resourceType, resourceConfig] of Object.entries(resources)) {
|
|
180
|
+
if (resourceType === 'sls') {
|
|
181
|
+
bindings.push(...buildLogBindings(resourceConfig));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const bindingMap = resourceConfig.bindings || {};
|
|
186
|
+
for (const [bindingKey, bindingValue] of Object.entries(bindingMap)) {
|
|
187
|
+
const converted = buildResourceBinding(resourceType, bindingKey, bindingValue, resourceConfig, aliasIndex);
|
|
188
|
+
if (converted) bindings.push(converted);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return bindings;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function convertSceneMappings(sceneData = {}) {
|
|
196
|
+
const mappings = sceneData.mappings || [];
|
|
197
|
+
return mappings.map((mapping) => ({
|
|
198
|
+
id: `sdd-${mapping.id || mapping.sceneName || Date.now()}`,
|
|
199
|
+
name: mapping.sceneName || mapping.name || 'Imported Scene',
|
|
200
|
+
keywords: mapping.keywords || [],
|
|
201
|
+
patterns: mapping.patterns || [],
|
|
202
|
+
workflow: {
|
|
203
|
+
skills: (mapping.skillSequence || []).map((step) => ({
|
|
204
|
+
skill: step.skill,
|
|
205
|
+
params: {
|
|
206
|
+
action: step.action,
|
|
207
|
+
...(step.params || {})
|
|
208
|
+
}
|
|
209
|
+
}))
|
|
210
|
+
},
|
|
211
|
+
preferences: mapping.userPreference || {},
|
|
212
|
+
stats: {
|
|
213
|
+
useCount: mapping.useCount || 0,
|
|
214
|
+
lastUsed: mapping.lastUsed || null,
|
|
215
|
+
successRate: typeof mapping.confidence === 'number' ? mapping.confidence : 1.0
|
|
216
|
+
},
|
|
217
|
+
metadata: {
|
|
218
|
+
source: 'sdd-agent',
|
|
219
|
+
originalId: mapping.id,
|
|
220
|
+
categoryHints: Object.keys(sceneData.sceneCategories || {}).filter((key) => (sceneData.sceneCategories[key] || []).includes(mapping.id)),
|
|
221
|
+
workflow: mapping.workflow || {}
|
|
222
|
+
}
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildComponentConfig(sddConfig) {
|
|
227
|
+
const resources = sddConfig.resources || {};
|
|
228
|
+
const components = {};
|
|
229
|
+
|
|
230
|
+
if (resources.sls?.enabled) {
|
|
231
|
+
const defaultEnv = resources.sls.defaultEnv || 'uat';
|
|
232
|
+
const envConfig = resources.sls.envConfig || {};
|
|
233
|
+
components.logService = {
|
|
234
|
+
default: 'aliyun-sls',
|
|
235
|
+
providers: {
|
|
236
|
+
'aliyun-sls': {
|
|
237
|
+
enabled: true,
|
|
238
|
+
mcpServer: normalizeMcpServer(resources.sls.mcpServer),
|
|
239
|
+
config: {
|
|
240
|
+
endpoints: {
|
|
241
|
+
test: envConfig.test?.endpoint || envConfig.uat?.endpoint,
|
|
242
|
+
prod: envConfig.prod?.endpoint || envConfig.gray?.endpoint
|
|
243
|
+
},
|
|
244
|
+
defaultProject: envConfig[defaultEnv]?.project,
|
|
245
|
+
defaultLogstore: envConfig[defaultEnv]?.logstore
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (resources.database?.enabled && resources.database.mcpServers?.dms) {
|
|
253
|
+
components.databaseManager = {
|
|
254
|
+
default: 'aliyun-dms',
|
|
255
|
+
providers: {
|
|
256
|
+
'aliyun-dms': {
|
|
257
|
+
enabled: true,
|
|
258
|
+
mcpServer: normalizeMcpServer(resources.database.mcpServers.dms)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (resources.redis?.enabled && resources.redis.mcpServers?.monitor) {
|
|
265
|
+
components.redisMonitor = {
|
|
266
|
+
default: 'aliyun-redis',
|
|
267
|
+
providers: {
|
|
268
|
+
'aliyun-redis': {
|
|
269
|
+
enabled: true,
|
|
270
|
+
mcpServer: normalizeMcpServer(resources.redis.mcpServers.monitor)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (resources.yapi?.enabled && resources.yapi.mcpServer) {
|
|
277
|
+
components.apiDoc = {
|
|
278
|
+
default: 'yapi',
|
|
279
|
+
providers: {
|
|
280
|
+
yapi: {
|
|
281
|
+
enabled: true,
|
|
282
|
+
mcpServer: normalizeMcpServer(resources.yapi.mcpServer)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (resources.yuque?.enabled && resources.yuque.mcpServer) {
|
|
289
|
+
components.knowledgeBase = {
|
|
290
|
+
default: 'yuque',
|
|
291
|
+
providers: {
|
|
292
|
+
yuque: {
|
|
293
|
+
enabled: true,
|
|
294
|
+
mcpServer: normalizeMcpServer(resources.yuque.mcpServer)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (resources.workflow?.enabled && resources.workflow.mcpServer) {
|
|
301
|
+
components.workflow = {
|
|
302
|
+
default: 'friday-flow',
|
|
303
|
+
providers: {
|
|
304
|
+
'friday-flow': {
|
|
305
|
+
enabled: true,
|
|
306
|
+
mcpServer: normalizeMcpServer(resources.workflow.mcpServer)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (resources.report?.enabled && resources.report.mcpServer) {
|
|
313
|
+
components.report = {
|
|
314
|
+
default: 'friday-report',
|
|
315
|
+
providers: {
|
|
316
|
+
'friday-report': {
|
|
317
|
+
enabled: true,
|
|
318
|
+
mcpServer: normalizeMcpServer(resources.report.mcpServer)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
version: '1.0.0',
|
|
326
|
+
components
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function mergeBindings(existingBindings = [], incomingBindings = []) {
|
|
331
|
+
const merged = [...existingBindings];
|
|
332
|
+
for (const binding of incomingBindings) {
|
|
333
|
+
const index = merged.findIndex((item) => item.id === binding.id
|
|
334
|
+
|| (item.business === binding.business
|
|
335
|
+
&& item.componentType === binding.componentType
|
|
336
|
+
&& item.provider === binding.provider));
|
|
337
|
+
if (index >= 0) {
|
|
338
|
+
merged[index] = {
|
|
339
|
+
...merged[index],
|
|
340
|
+
...binding,
|
|
341
|
+
aliases: [...new Set([...(merged[index].aliases || []), ...(binding.aliases || [])])],
|
|
342
|
+
keywords: [...new Set([...(merged[index].keywords || []), ...(binding.keywords || [])])],
|
|
343
|
+
connection: {
|
|
344
|
+
...(merged[index].connection || {}),
|
|
345
|
+
...(binding.connection || {})
|
|
346
|
+
},
|
|
347
|
+
metadata: {
|
|
348
|
+
...(merged[index].metadata || {}),
|
|
349
|
+
...(binding.metadata || {})
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
} else {
|
|
353
|
+
merged.push(binding);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return merged;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function mergeScenes(existingScenes = [], incomingScenes = []) {
|
|
360
|
+
const merged = [...existingScenes];
|
|
361
|
+
for (const scene of incomingScenes) {
|
|
362
|
+
const index = merged.findIndex((item) => item.id === scene.id || item.name === scene.name);
|
|
363
|
+
if (index >= 0) {
|
|
364
|
+
merged[index] = {
|
|
365
|
+
...merged[index],
|
|
366
|
+
...scene,
|
|
367
|
+
keywords: [...new Set([...(merged[index].keywords || []), ...(scene.keywords || [])])],
|
|
368
|
+
patterns: [...new Set([...(merged[index].patterns || []), ...(scene.patterns || [])])],
|
|
369
|
+
workflow: scene.workflow || merged[index].workflow,
|
|
370
|
+
preferences: {
|
|
371
|
+
...(merged[index].preferences || {}),
|
|
372
|
+
...(scene.preferences || {})
|
|
373
|
+
},
|
|
374
|
+
metadata: {
|
|
375
|
+
...(merged[index].metadata || {}),
|
|
376
|
+
...(scene.metadata || {})
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
} else {
|
|
380
|
+
merged.push(scene);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return merged;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function readJsonIfExists(filePath, fallback) {
|
|
387
|
+
if (!(await fs.pathExists(filePath))) return fallback;
|
|
388
|
+
return fs.readJson(filePath);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function syncSddAgentToFlowMind(options = {}) {
|
|
392
|
+
const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
|
|
393
|
+
const targetHome = options.targetHome || getHomeDir();
|
|
394
|
+
const sourceConfigPath = path.join(sourceDir, 'resource-config.json');
|
|
395
|
+
const sourceScenesPath = path.join(sourceDir, 'learning', 'scene-mappings.json');
|
|
396
|
+
const targetDir = path.join(targetHome, '.flowmind');
|
|
397
|
+
const targetLearningDir = path.join(targetDir, 'learning');
|
|
398
|
+
|
|
399
|
+
if (!(await fs.pathExists(sourceConfigPath))) {
|
|
400
|
+
throw new Error(`SDD-Agent resource config not found: ${sourceConfigPath}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const sddConfig = await fs.readJson(sourceConfigPath);
|
|
404
|
+
const sddScenes = await readJsonIfExists(sourceScenesPath, { mappings: [] });
|
|
405
|
+
|
|
406
|
+
const targetResourceConfigPath = path.join(targetDir, 'resource-config.json');
|
|
407
|
+
const targetComponentConfigPath = path.join(targetDir, 'component-config.json');
|
|
408
|
+
const targetBindingsPath = path.join(targetLearningDir, 'resource-bindings.json');
|
|
409
|
+
const targetScenesPath = path.join(targetLearningDir, 'scenes.json');
|
|
410
|
+
|
|
411
|
+
const existingResourceConfig = await readJsonIfExists(targetResourceConfigPath, {});
|
|
412
|
+
const existingComponentConfig = await readJsonIfExists(targetComponentConfigPath, { version: '1.0.0', components: {} });
|
|
413
|
+
const existingBindings = await readJsonIfExists(targetBindingsPath, { version: '1.0', bindings: [] });
|
|
414
|
+
const existingScenes = await readJsonIfExists(targetScenesPath, { version: '1.0', mappings: [] });
|
|
415
|
+
|
|
416
|
+
const nextResourceConfig = deepMerge(existingResourceConfig, sanitizeResourceConfig(sddConfig));
|
|
417
|
+
const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig));
|
|
418
|
+
const importedBindings = convertResourceBindings(sddConfig);
|
|
419
|
+
const nextBindings = {
|
|
420
|
+
version: existingBindings.version || '1.0',
|
|
421
|
+
lastUpdated: new Date().toISOString(),
|
|
422
|
+
bindings: mergeBindings(existingBindings.bindings || [], importedBindings)
|
|
423
|
+
};
|
|
424
|
+
const importedScenes = convertSceneMappings(sddScenes);
|
|
425
|
+
const nextScenes = {
|
|
426
|
+
version: existingScenes.version || '1.0',
|
|
427
|
+
lastUpdated: new Date().toISOString(),
|
|
428
|
+
mappings: mergeScenes(existingScenes.mappings || [], importedScenes)
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
await fs.ensureDir(targetLearningDir);
|
|
432
|
+
await fs.writeJson(targetResourceConfigPath, nextResourceConfig, { spaces: 2 });
|
|
433
|
+
await fs.writeJson(targetComponentConfigPath, nextComponentConfig, { spaces: 2 });
|
|
434
|
+
await fs.writeJson(targetBindingsPath, nextBindings, { spaces: 2 });
|
|
435
|
+
await fs.writeJson(targetScenesPath, nextScenes, { spaces: 2 });
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
sourceDir,
|
|
439
|
+
targetDir,
|
|
440
|
+
files: {
|
|
441
|
+
resourceConfig: targetResourceConfigPath,
|
|
442
|
+
componentConfig: targetComponentConfigPath,
|
|
443
|
+
resourceBindings: targetBindingsPath,
|
|
444
|
+
scenes: targetScenesPath
|
|
445
|
+
},
|
|
446
|
+
counts: {
|
|
447
|
+
importedBindings: importedBindings.length,
|
|
448
|
+
totalBindings: nextBindings.bindings.length,
|
|
449
|
+
importedScenes: importedScenes.length,
|
|
450
|
+
totalScenes: nextScenes.mappings.length,
|
|
451
|
+
components: Object.keys(nextComponentConfig.components || {}).length
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = {
|
|
457
|
+
buildAliasIndex,
|
|
458
|
+
buildComponentConfig,
|
|
459
|
+
convertResourceBindings,
|
|
460
|
+
convertSceneMappings,
|
|
461
|
+
deepMerge,
|
|
462
|
+
mergeBindings,
|
|
463
|
+
mergeScenes,
|
|
464
|
+
normalizeMcpServer,
|
|
465
|
+
sanitizeResourceConfig,
|
|
466
|
+
syncSddAgentToFlowMind
|
|
467
|
+
};
|
package/core/skill-loader.js
CHANGED
|
@@ -255,14 +255,19 @@ class SkillLoader {
|
|
|
255
255
|
for (const [name, skill] of this.skills) {
|
|
256
256
|
try {
|
|
257
257
|
const canHandle = await skill.canHandle(input, context);
|
|
258
|
-
|
|
258
|
+
const fallbackMatch = this.matchDefinitionSignals(skill, input);
|
|
259
|
+
|
|
260
|
+
if (canHandle || fallbackMatch.matched) {
|
|
259
261
|
candidates.push({
|
|
260
262
|
name: skill.name,
|
|
261
263
|
skill: skill,
|
|
262
264
|
description: skill.definition?.description || '',
|
|
263
265
|
triggers: skill.definition?.triggers || [],
|
|
264
266
|
category: skill.definition?.category || skill.definition?.metadata?.category || '',
|
|
265
|
-
score: this.calculateSkillScore(skill, input, context
|
|
267
|
+
score: this.calculateSkillScore(skill, input, context, {
|
|
268
|
+
canHandle,
|
|
269
|
+
...fallbackMatch
|
|
270
|
+
})
|
|
266
271
|
});
|
|
267
272
|
}
|
|
268
273
|
} catch (error) {
|
|
@@ -276,19 +281,16 @@ class SkillLoader {
|
|
|
276
281
|
/**
|
|
277
282
|
* Calculate skill score for selection
|
|
278
283
|
*/
|
|
279
|
-
calculateSkillScore(skill, input, context) {
|
|
284
|
+
calculateSkillScore(skill, input, context, matchInfo = {}) {
|
|
280
285
|
let score = 0;
|
|
281
286
|
|
|
282
287
|
// Base score for matching
|
|
283
|
-
score += 10;
|
|
288
|
+
score += matchInfo.canHandle ? 10 : 4;
|
|
284
289
|
|
|
285
290
|
// Bonus for trigger specificity
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
score += 5;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
291
|
+
score += (matchInfo.triggerHits?.length || 0) * 5;
|
|
292
|
+
score += (matchInfo.nameHits?.length || 0) * 4;
|
|
293
|
+
score += matchInfo.categoryHit ? 2 : 0;
|
|
292
294
|
|
|
293
295
|
// Bonus for recent successful use
|
|
294
296
|
const bindings = this.learning?.skillBindings?.bindings?.[skill.name];
|
|
@@ -299,6 +301,45 @@ class SkillLoader {
|
|
|
299
301
|
return score;
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
matchDefinitionSignals(skill, input) {
|
|
305
|
+
if (!input) {
|
|
306
|
+
return {
|
|
307
|
+
matched: false,
|
|
308
|
+
triggerHits: [],
|
|
309
|
+
nameHits: [],
|
|
310
|
+
categoryHit: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const inputLower = input.toLowerCase();
|
|
315
|
+
const triggers = skill.definition?.triggers || [];
|
|
316
|
+
const triggerHits = triggers.filter((trigger) => inputLower.includes(String(trigger).toLowerCase()));
|
|
317
|
+
|
|
318
|
+
const nameVariants = this.getSkillNameVariants(skill.name);
|
|
319
|
+
const nameHits = nameVariants.filter((variant) => inputLower.includes(variant));
|
|
320
|
+
|
|
321
|
+
const category = skill.definition?.category || skill.definition?.metadata?.category || '';
|
|
322
|
+
const categoryHit = category ? inputLower.includes(String(category).toLowerCase()) : false;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
matched: triggerHits.length > 0 || nameHits.length > 0 || categoryHit,
|
|
326
|
+
triggerHits,
|
|
327
|
+
nameHits,
|
|
328
|
+
categoryHit
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getSkillNameVariants(name = '') {
|
|
333
|
+
const normalized = String(name).toLowerCase();
|
|
334
|
+
return [...new Set([
|
|
335
|
+
normalized,
|
|
336
|
+
normalized.replace(/-/g, ' '),
|
|
337
|
+
normalized.replace(/-/g, ''),
|
|
338
|
+
normalized.replace(/_/g, ' '),
|
|
339
|
+
normalized.replace(/_/g, '')
|
|
340
|
+
])].filter(Boolean);
|
|
341
|
+
}
|
|
342
|
+
|
|
302
343
|
/**
|
|
303
344
|
* Get skill by name
|
|
304
345
|
*/
|
package/core/utils.js
CHANGED
|
@@ -15,4 +15,58 @@ function expandPath(filePath) {
|
|
|
15
15
|
return filePath;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
function detectManagedCliHost(env = process.env) {
|
|
19
|
+
if (env.FLOWMIND_ALLOW_NESTED_TUI === '1') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (env.CODEX_THREAD_ID || env.CODEX_MANAGED_PACKAGE_ROOT || env.OPENAI_CODEX === '1') {
|
|
24
|
+
return {
|
|
25
|
+
id: 'codex',
|
|
26
|
+
name: 'Codex CLI'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (env.CLAUDECODE === '1' || env.CLAUDE_CODE === '1' || env.CLAUDECODE_ENTRYPOINT) {
|
|
31
|
+
return {
|
|
32
|
+
id: 'claude-code',
|
|
33
|
+
name: 'Claude Code'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shouldUseAsciiUi(env = process.env) {
|
|
41
|
+
if (env.FLOWMIND_FORCE_UNICODE_UI === '1') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (env.FLOWMIND_ASCII_UI === '1') {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return env.TERM_PROGRAM === 'Apple_Terminal';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldProxyInkStdin(stdin = process.stdin) {
|
|
53
|
+
return !stdin || stdin.isTTY !== true || typeof stdin.setRawMode !== 'function';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getNestedTuiGuardMessage(host) {
|
|
57
|
+
const hostName = host?.name || 'this host CLI';
|
|
58
|
+
return [
|
|
59
|
+
`${hostName} already controls raw stdin and the fullscreen terminal surface.`,
|
|
60
|
+
'Launching FlowMind TUI/Dashboard inside it can terminate the outer session as soon as input is captured.',
|
|
61
|
+
'Use `flowmind-codex` for JSON-based integration, or run `flowmind tui` in a standalone terminal.',
|
|
62
|
+
'Set `FLOWMIND_ALLOW_NESTED_TUI=1` only if you explicitly want to bypass this safeguard.'
|
|
63
|
+
].join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
expandPath,
|
|
68
|
+
detectManagedCliHost,
|
|
69
|
+
shouldUseAsciiUi,
|
|
70
|
+
shouldProxyInkStdin,
|
|
71
|
+
getNestedTuiGuardMessage
|
|
72
|
+
};
|
package/dashboard/app.jsx
CHANGED
|
@@ -5,7 +5,7 @@ const StatsRow = require('./components/StatsRow.jsx');
|
|
|
5
5
|
const DragonPanel = require('./components/DragonPanel.jsx');
|
|
6
6
|
const McpStatusBar = require('./components/McpStatusBar.jsx');
|
|
7
7
|
|
|
8
|
-
function DashboardApp({ flowmind, eventBus }) {
|
|
8
|
+
function DashboardApp({ flowmind, eventBus, asciiMode = false }) {
|
|
9
9
|
const { exit } = useApp();
|
|
10
10
|
|
|
11
11
|
useInput((input, key) => {
|
|
@@ -15,13 +15,13 @@ function DashboardApp({ flowmind, eventBus }) {
|
|
|
15
15
|
return (
|
|
16
16
|
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
17
17
|
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
18
|
-
React.createElement(ActivityFeed, { eventBus: eventBus }),
|
|
18
|
+
React.createElement(ActivityFeed, { eventBus: eventBus, asciiMode: asciiMode }),
|
|
19
19
|
React.createElement(Box, { flexDirection: 'column', width: '60%', flexGrow: 1 },
|
|
20
|
-
React.createElement(StatsRow, { flowmind: flowmind }),
|
|
21
|
-
React.createElement(DragonPanel, { flowmind: flowmind })
|
|
20
|
+
React.createElement(StatsRow, { flowmind: flowmind, asciiMode: asciiMode }),
|
|
21
|
+
React.createElement(DragonPanel, { flowmind: flowmind, asciiMode: asciiMode })
|
|
22
22
|
)
|
|
23
23
|
),
|
|
24
|
-
React.createElement(McpStatusBar, { eventBus: eventBus })
|
|
24
|
+
React.createElement(McpStatusBar, { eventBus: eventBus, asciiMode: asciiMode })
|
|
25
25
|
)
|
|
26
26
|
);
|
|
27
27
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
|
+
const { getBorderStyle, getCheckMark } = require('../../tui/ui');
|
|
3
4
|
|
|
4
5
|
const EVENT_COLORS = {
|
|
5
6
|
'skill:executed': 'green',
|
|
@@ -16,16 +17,16 @@ function formatTime(timestamp) {
|
|
|
16
17
|
return new Date(timestamp).toTimeString().substring(0, 8);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function formatEvent(event) {
|
|
20
|
+
function formatEvent(event, asciiMode) {
|
|
20
21
|
switch (event.type) {
|
|
21
22
|
case 'skill:executed':
|
|
22
|
-
return 'skill:' + (event.data?.name || '?') + ' ' + (event.data?.success
|
|
23
|
+
return 'skill:' + (event.data?.name || '?') + ' ' + getCheckMark(event.data?.success, asciiMode);
|
|
23
24
|
case 'honor:awarded':
|
|
24
25
|
return 'honor +' + (event.data?.points || 0) + ' (' + (event.data?.description || '') + ')';
|
|
25
26
|
case 'learning:recorded':
|
|
26
27
|
return 'learning:' + (event.data?.type || '?') + ' ' + (event.data?.skill || '');
|
|
27
28
|
case 'mcp:tool_called':
|
|
28
|
-
return 'MCP:' + (event.data?.tool || '?') + ' ' + (event.data?.success
|
|
29
|
+
return 'MCP:' + (event.data?.tool || '?') + ' ' + getCheckMark(event.data?.success, asciiMode) + ' ' + (event.data?.duration || 0) + 'ms';
|
|
29
30
|
case 'process:start':
|
|
30
31
|
return 'process: ' + (event.data?.input?.substring(0, 30) || '?') + '...';
|
|
31
32
|
case 'process:complete':
|
|
@@ -37,7 +38,7 @@ function formatEvent(event) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function ActivityFeed({ eventBus }) {
|
|
41
|
+
function ActivityFeed({ eventBus, asciiMode = false }) {
|
|
41
42
|
const [events, setEvents] = React.useState([]);
|
|
42
43
|
|
|
43
44
|
React.useEffect(() => {
|
|
@@ -68,14 +69,14 @@ function ActivityFeed({ eventBus }) {
|
|
|
68
69
|
const displayEvents = events.slice(-30);
|
|
69
70
|
|
|
70
71
|
return (
|
|
71
|
-
React.createElement(Box, { flexDirection: 'column', borderStyle:
|
|
72
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'green', paddingX: 1, width: '40%' },
|
|
72
73
|
React.createElement(Text, { bold: true, color: 'green' }, 'Activity Feed'),
|
|
73
74
|
React.createElement(Box, { flexDirection: 'column', marginTop: 1, overflow: 'hidden' },
|
|
74
75
|
displayEvents.length === 0 && React.createElement(Text, { color: 'gray' }, 'Waiting for events...'),
|
|
75
76
|
displayEvents.map((event, i) =>
|
|
76
77
|
React.createElement(Text, { key: i },
|
|
77
78
|
React.createElement(Text, { color: 'gray' }, formatTime(event.timestamp) + ' '),
|
|
78
|
-
React.createElement(Text, { color: EVENT_COLORS[event.type] || 'white' }, formatEvent(event))
|
|
79
|
+
React.createElement(Text, { color: EVENT_COLORS[event.type] || 'white' }, formatEvent(event, asciiMode))
|
|
79
80
|
)
|
|
80
81
|
)
|
|
81
82
|
)
|