flowmind 1.4.7 → 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.
@@ -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
+ };
@@ -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
- if (canHandle) {
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
- const triggers = skill.definition?.triggers || [];
287
- for (const trigger of triggers) {
288
- if (input.toLowerCase().includes(trigger.toLowerCase())) {
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
- module.exports = { expandPath };
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 ? '\u2713' : '\u2717');
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 ? '\u2713' : '\u2717') + ' ' + (event.data?.duration || 0) + 'ms';
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: 'single', borderColor: 'green', paddingX: 1, width: '40%' },
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
  )