flowmind 1.5.1 → 1.5.3

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.
@@ -2,10 +2,14 @@ const fs = require('fs-extra');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
 
5
- const MCP_SERVER_ALIASES = Object.freeze({
6
- 'aomi-yapi-mcp': 'yapi-mcp',
7
- 'aomi-yuque-mcp': 'yuque-mcp'
8
- });
5
+ const SDD_SOURCE_SYNC_FILES = Object.freeze([
6
+ 'RESOURCE_INDEX.md',
7
+ 'project-db-configs.json',
8
+ 'project-git-map.json',
9
+ 'feign-link-map.json',
10
+ 'auto-flow-apis.md',
11
+ 'auto-flow-pipeline-map.json'
12
+ ]);
9
13
 
10
14
  const RESOURCE_BINDING_SPECS = Object.freeze({
11
15
  yapi: { componentType: 'apiDoc', provider: 'yapi' },
@@ -23,7 +27,9 @@ function getHomeDir() {
23
27
 
24
28
  function normalizeMcpServer(name) {
25
29
  if (!name) return null;
26
- return MCP_SERVER_ALIASES[name] || name;
30
+ if (/^[^-]+-yapi-mcp$/i.test(name)) return 'yapi-mcp';
31
+ if (/^[^-]+-yuque-mcp$/i.test(name)) return 'yuque-mcp';
32
+ return name;
27
33
  }
28
34
 
29
35
  function deepMerge(target, source) {
@@ -46,11 +52,19 @@ function isObject(value) {
46
52
  return value !== null && typeof value === 'object' && !Array.isArray(value);
47
53
  }
48
54
 
49
- function sanitizeResourceConfig(sddConfig) {
55
+ function sanitizeResourceConfig(sddConfig, skillBindingData = {}) {
56
+ const resources = { ...(sddConfig.resources || {}) };
57
+ if (!resources.workflow) {
58
+ const workflowResource = buildWorkflowResource(skillBindingData);
59
+ if (workflowResource) {
60
+ resources.workflow = workflowResource;
61
+ }
62
+ }
63
+
50
64
  return {
51
65
  version: sddConfig.version || '1.0',
52
66
  lastUpdated: new Date().toISOString(),
53
- resources: sddConfig.resources || {},
67
+ resources,
54
68
  mcpServers: sddConfig.mcpServers || {},
55
69
  aliases: sddConfig.aliases || {},
56
70
  metadata: {
@@ -171,7 +185,169 @@ function buildLogBindings(resourceConfig = {}) {
171
185
  }));
172
186
  }
173
187
 
174
- function convertResourceBindings(sddConfig) {
188
+ function buildWorkflowResource(skillBindingData = {}) {
189
+ const workflowBinding = skillBindingData.bindings?.['auto-flow'];
190
+ if (!workflowBinding) return null;
191
+
192
+ const learningCount = workflowBinding.learningCount || (workflowBinding.records || []).length || (workflowBinding.rules || []).length;
193
+
194
+ return {
195
+ enabled: true,
196
+ mcpServer: 'friday-auto-flow',
197
+ bindings: {
198
+ 'auto-flow': {
199
+ name: '自动部署',
200
+ skill: 'auto-flow',
201
+ provider: 'friday-flow',
202
+ mcpServer: 'friday-auto-flow',
203
+ learningCount,
204
+ lastLearning: workflowBinding.lastLearning || null,
205
+ records: workflowBinding.records || [],
206
+ rules: workflowBinding.rules || []
207
+ }
208
+ },
209
+ defaultBinding: 'auto-flow'
210
+ };
211
+ }
212
+
213
+ function normalizeWorkflowTransport(transport) {
214
+ if (!transport || !transport.url) return null;
215
+
216
+ const headers = transport.headers || {};
217
+ const normalizedHeaders = {};
218
+ for (const [key, value] of Object.entries(headers)) {
219
+ if (value === undefined || value === null || value === '') continue;
220
+ normalizedHeaders[key] = value;
221
+ }
222
+
223
+ return {
224
+ url: transport.url,
225
+ type: transport.type || 'http',
226
+ headers: normalizedHeaders,
227
+ description: transport.description || 'Workflow MCP server'
228
+ };
229
+ }
230
+
231
+ async function discoverWorkflowTransport(options = {}) {
232
+ const direct = normalizeWorkflowTransport(options.workflowTransport);
233
+ if (direct) return direct;
234
+
235
+ const envUrl = process.env.FLOWMIND_AUTO_FLOW_MCP_URL;
236
+ if (envUrl) {
237
+ return normalizeWorkflowTransport({
238
+ url: envUrl,
239
+ type: 'http',
240
+ headers: process.env.FLOWMIND_AUTO_FLOW_MCP_TOKEN
241
+ ? { mcp_token: process.env.FLOWMIND_AUTO_FLOW_MCP_TOKEN }
242
+ : {},
243
+ description: 'Workflow MCP server from environment'
244
+ });
245
+ }
246
+
247
+ const debugDir = path.join(os.homedir(), '.claude', 'debug');
248
+ if (!await fs.pathExists(debugDir)) return null;
249
+
250
+ const entries = await fs.readdir(debugDir);
251
+ const candidates = [];
252
+ for (const entry of entries) {
253
+ const filePath = path.join(debugDir, entry);
254
+ try {
255
+ const stat = await fs.stat(filePath);
256
+ if (stat.isFile()) {
257
+ candidates.push({ filePath, mtimeMs: stat.mtimeMs });
258
+ }
259
+ } catch (error) {
260
+ // skip unreadable files
261
+ }
262
+ }
263
+
264
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
265
+
266
+ for (const { filePath } of candidates.slice(0, 10)) {
267
+ try {
268
+ const content = await fs.readFile(filePath, 'utf8');
269
+ const match = content.match(/MCP server "friday-auto-flow": HTTP transport options: (\{[^\n]+\})/);
270
+ if (!match) continue;
271
+
272
+ const parsed = JSON.parse(match[1]);
273
+ return normalizeWorkflowTransport({
274
+ url: parsed.url,
275
+ type: parsed.type || 'http',
276
+ headers: parsed.headers || {},
277
+ description: 'Workflow MCP server discovered from Claude debug logs'
278
+ });
279
+ } catch (error) {
280
+ // continue scanning other files
281
+ }
282
+ }
283
+
284
+ return null;
285
+ }
286
+
287
+ function buildWorkflowBindings(skillBindingData = {}, workflowTransport = null) {
288
+ const workflowBinding = skillBindingData.bindings?.['auto-flow'];
289
+ if (!workflowBinding) return [];
290
+
291
+ const ruleSummaries = (workflowBinding.records || []).map((record) => record.summary).filter(Boolean);
292
+ const ruleActions = (workflowBinding.rules || []).map((rule) => rule.action).filter(Boolean);
293
+ const keywords = [
294
+ 'auto-flow',
295
+ 'workflow',
296
+ 'pipeline',
297
+ 'deploy',
298
+ 'deployment',
299
+ '部署',
300
+ '发布',
301
+ '上线',
302
+ '流水线',
303
+ ...ruleSummaries,
304
+ ...ruleActions
305
+ ];
306
+
307
+ return [{
308
+ id: 'sdd-workflow-auto-flow',
309
+ timestamp: new Date().toISOString(),
310
+ business: '自动部署',
311
+ aliases: [
312
+ 'auto-flow',
313
+ 'workflow',
314
+ 'pipeline',
315
+ '飞流',
316
+ '飞流部署',
317
+ '部署',
318
+ '发布',
319
+ '上线',
320
+ '流水线'
321
+ ],
322
+ componentType: 'workflow',
323
+ provider: 'friday-flow',
324
+ mcpServer: 'friday-auto-flow',
325
+ source: 'sdd-agent-sync',
326
+ keywords: [...new Set(keywords.filter(Boolean))],
327
+ connection: {
328
+ skill: 'auto-flow',
329
+ learningCount: workflowBinding.learningCount || 0,
330
+ lastLearning: workflowBinding.lastLearning || null,
331
+ records: workflowBinding.records || [],
332
+ rules: workflowBinding.rules || [],
333
+ transport: workflowTransport ? {
334
+ url: workflowTransport.url,
335
+ type: workflowTransport.type
336
+ } : undefined
337
+ },
338
+ metadata: {
339
+ source: 'sdd-agent',
340
+ resourceType: 'workflow',
341
+ bindingKey: 'auto-flow'
342
+ },
343
+ stats: {
344
+ useCount: 0,
345
+ lastUsed: null
346
+ }
347
+ }];
348
+ }
349
+
350
+ function convertResourceBindings(sddConfig, skillBindingData = {}, workflowTransport = null) {
175
351
  const resources = sddConfig.resources || {};
176
352
  const aliasIndex = buildAliasIndex(sddConfig.aliases);
177
353
  const bindings = [];
@@ -189,6 +365,8 @@ function convertResourceBindings(sddConfig) {
189
365
  }
190
366
  }
191
367
 
368
+ bindings.push(...buildWorkflowBindings(skillBindingData, workflowTransport));
369
+
192
370
  return bindings;
193
371
  }
194
372
 
@@ -223,9 +401,13 @@ function convertSceneMappings(sceneData = {}) {
223
401
  }));
224
402
  }
225
403
 
226
- function buildComponentConfig(sddConfig) {
404
+ function buildComponentConfig(sddConfig, skillBindingData = {}, workflowTransport = null) {
227
405
  const resources = sddConfig.resources || {};
406
+ const workflowResource = resources.workflow || buildWorkflowResource(skillBindingData);
228
407
  const components = {};
408
+ const directQueryServer = resources.database?.mcpServers?.directQuery
409
+ || resources.redis?.mcpServers?.directQuery
410
+ || null;
229
411
 
230
412
  if (resources.sls?.enabled) {
231
413
  const defaultEnv = resources.sls.defaultEnv || 'uat';
@@ -261,6 +443,18 @@ function buildComponentConfig(sddConfig) {
261
443
  };
262
444
  }
263
445
 
446
+ if ((resources.database?.enabled || resources.redis?.enabled) && directQueryServer) {
447
+ components.databaseQuery = {
448
+ default: 'aliyun-rds-query',
449
+ providers: {
450
+ 'aliyun-rds-query': {
451
+ enabled: true,
452
+ mcpServer: normalizeMcpServer(directQueryServer)
453
+ }
454
+ }
455
+ };
456
+ }
457
+
264
458
  if (resources.redis?.enabled && resources.redis.mcpServers?.monitor) {
265
459
  components.redisMonitor = {
266
460
  default: 'aliyun-redis',
@@ -297,13 +491,14 @@ function buildComponentConfig(sddConfig) {
297
491
  };
298
492
  }
299
493
 
300
- if (resources.workflow?.enabled && resources.workflow.mcpServer) {
494
+ if (workflowResource?.enabled && workflowResource.mcpServer) {
301
495
  components.workflow = {
302
496
  default: 'friday-flow',
303
497
  providers: {
304
498
  'friday-flow': {
305
499
  enabled: true,
306
- mcpServer: normalizeMcpServer(resources.workflow.mcpServer)
500
+ mcpServer: normalizeMcpServer(workflowResource.mcpServer),
501
+ ...(workflowTransport ? { transport: workflowTransport } : {})
307
502
  }
308
503
  }
309
504
  };
@@ -388,11 +583,124 @@ async function readJsonIfExists(filePath, fallback) {
388
583
  return fs.readJson(filePath);
389
584
  }
390
585
 
586
+ async function statIfExists(filePath) {
587
+ try {
588
+ return await fs.stat(filePath);
589
+ } catch (error) {
590
+ return null;
591
+ }
592
+ }
593
+
594
+ async function copySelectedSourceFiles(sourceDir, targetDir) {
595
+ const sourceRoot = path.join(sourceDir, 'source');
596
+ const copied = [];
597
+
598
+ if (!(await fs.pathExists(sourceRoot))) {
599
+ return copied;
600
+ }
601
+
602
+ await fs.ensureDir(targetDir);
603
+
604
+ for (const fileName of SDD_SOURCE_SYNC_FILES) {
605
+ const sourcePath = path.join(sourceRoot, fileName);
606
+ if (!(await fs.pathExists(sourcePath))) {
607
+ continue;
608
+ }
609
+
610
+ const targetPath = path.join(targetDir, fileName);
611
+ await fs.copy(sourcePath, targetPath, { overwrite: true, dereference: true });
612
+ copied.push({
613
+ name: fileName,
614
+ sourcePath,
615
+ targetPath
616
+ });
617
+ }
618
+
619
+ return copied;
620
+ }
621
+
622
+ async function buildSyncSignature(sourceDir) {
623
+ const trackedFiles = [
624
+ path.join(sourceDir, 'resource-config.json'),
625
+ path.join(sourceDir, 'learning', 'scene-mappings.json'),
626
+ path.join(sourceDir, 'learning', 'skill-bindings.json'),
627
+ ...SDD_SOURCE_SYNC_FILES.map((fileName) => path.join(sourceDir, 'source', fileName))
628
+ ];
629
+
630
+ const entries = [];
631
+ for (const filePath of trackedFiles) {
632
+ const stat = await statIfExists(filePath);
633
+ if (!stat) continue;
634
+ entries.push({
635
+ path: filePath,
636
+ size: stat.size,
637
+ mtimeMs: stat.mtimeMs
638
+ });
639
+ }
640
+
641
+ entries.sort((a, b) => a.path.localeCompare(b.path));
642
+ return JSON.stringify(entries);
643
+ }
644
+
645
+ async function autoSyncSddAgentToFlowMind(options = {}) {
646
+ if (process.env.FLOWMIND_DISABLE_SDD_AGENT_AUTO_SYNC === '1') {
647
+ return { synced: false, skipped: true, reason: 'disabled' };
648
+ }
649
+
650
+ const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
651
+ const targetHome = options.targetHome || getHomeDir();
652
+ const sourceConfigPath = path.join(sourceDir, 'resource-config.json');
653
+
654
+ if (!(await fs.pathExists(sourceConfigPath))) {
655
+ return { synced: false, skipped: true, reason: 'missing-source-config' };
656
+ }
657
+
658
+ const targetDir = path.join(targetHome, '.flowmind');
659
+ const statePath = path.join(targetDir, 'sdd-agent-sync-state.json');
660
+ const signature = await buildSyncSignature(sourceDir);
661
+ const previousState = await readJsonIfExists(statePath, null);
662
+
663
+ if (previousState?.signature === signature) {
664
+ return {
665
+ synced: false,
666
+ skipped: true,
667
+ reason: 'up-to-date',
668
+ sourceDir,
669
+ targetDir,
670
+ statePath
671
+ };
672
+ }
673
+
674
+ const summary = await syncSddAgentToFlowMind({
675
+ ...options,
676
+ sourceDir,
677
+ targetHome
678
+ });
679
+
680
+ await fs.ensureDir(targetDir);
681
+ await fs.writeJson(statePath, {
682
+ sourceDir,
683
+ targetDir,
684
+ signature,
685
+ syncedAt: new Date().toISOString(),
686
+ files: summary.files,
687
+ counts: summary.counts
688
+ }, { spaces: 2 });
689
+
690
+ return {
691
+ synced: true,
692
+ skipped: false,
693
+ statePath,
694
+ ...summary
695
+ };
696
+ }
697
+
391
698
  async function syncSddAgentToFlowMind(options = {}) {
392
699
  const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
393
700
  const targetHome = options.targetHome || getHomeDir();
394
701
  const sourceConfigPath = path.join(sourceDir, 'resource-config.json');
395
702
  const sourceScenesPath = path.join(sourceDir, 'learning', 'scene-mappings.json');
703
+ const sourceSkillBindingsPath = path.join(sourceDir, 'learning', 'skill-bindings.json');
396
704
  const targetDir = path.join(targetHome, '.flowmind');
397
705
  const targetLearningDir = path.join(targetDir, 'learning');
398
706
 
@@ -402,20 +710,30 @@ async function syncSddAgentToFlowMind(options = {}) {
402
710
 
403
711
  const sddConfig = await fs.readJson(sourceConfigPath);
404
712
  const sddScenes = await readJsonIfExists(sourceScenesPath, { mappings: [] });
713
+ const sddSkillBindings = await readJsonIfExists(sourceSkillBindingsPath, { version: '1.0', bindings: {} });
714
+ sddConfig.skillBindings = sddSkillBindings;
715
+ const workflowTransport = await discoverWorkflowTransport(options);
716
+ if (workflowTransport) {
717
+ sddConfig.mcpServers = {
718
+ ...(sddConfig.mcpServers || {}),
719
+ 'friday-auto-flow': workflowTransport
720
+ };
721
+ }
405
722
 
406
723
  const targetResourceConfigPath = path.join(targetDir, 'resource-config.json');
407
724
  const targetComponentConfigPath = path.join(targetDir, 'component-config.json');
408
725
  const targetBindingsPath = path.join(targetLearningDir, 'resource-bindings.json');
409
726
  const targetScenesPath = path.join(targetLearningDir, 'scenes.json');
727
+ const targetSourceDir = path.join(targetDir, 'source');
410
728
 
411
729
  const existingResourceConfig = await readJsonIfExists(targetResourceConfigPath, {});
412
730
  const existingComponentConfig = await readJsonIfExists(targetComponentConfigPath, { version: '1.0.0', components: {} });
413
731
  const existingBindings = await readJsonIfExists(targetBindingsPath, { version: '1.0', bindings: [] });
414
732
  const existingScenes = await readJsonIfExists(targetScenesPath, { version: '1.0', mappings: [] });
415
733
 
416
- const nextResourceConfig = deepMerge(existingResourceConfig, sanitizeResourceConfig(sddConfig));
417
- const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig));
418
- const importedBindings = convertResourceBindings(sddConfig);
734
+ const nextResourceConfig = deepMerge(existingResourceConfig, sanitizeResourceConfig(sddConfig, sddSkillBindings));
735
+ const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig, sddSkillBindings, workflowTransport));
736
+ const importedBindings = convertResourceBindings(sddConfig, sddSkillBindings, workflowTransport);
419
737
  const nextBindings = {
420
738
  version: existingBindings.version || '1.0',
421
739
  lastUpdated: new Date().toISOString(),
@@ -433,6 +751,7 @@ async function syncSddAgentToFlowMind(options = {}) {
433
751
  await fs.writeJson(targetComponentConfigPath, nextComponentConfig, { spaces: 2 });
434
752
  await fs.writeJson(targetBindingsPath, nextBindings, { spaces: 2 });
435
753
  await fs.writeJson(targetScenesPath, nextScenes, { spaces: 2 });
754
+ const copiedSourceFiles = await copySelectedSourceFiles(sourceDir, targetSourceDir);
436
755
 
437
756
  return {
438
757
  sourceDir,
@@ -441,14 +760,16 @@ async function syncSddAgentToFlowMind(options = {}) {
441
760
  resourceConfig: targetResourceConfigPath,
442
761
  componentConfig: targetComponentConfigPath,
443
762
  resourceBindings: targetBindingsPath,
444
- scenes: targetScenesPath
763
+ scenes: targetScenesPath,
764
+ sourceFiles: copiedSourceFiles.map((item) => item.targetPath)
445
765
  },
446
766
  counts: {
447
767
  importedBindings: importedBindings.length,
448
768
  totalBindings: nextBindings.bindings.length,
449
769
  importedScenes: importedScenes.length,
450
770
  totalScenes: nextScenes.mappings.length,
451
- components: Object.keys(nextComponentConfig.components || {}).length
771
+ components: Object.keys(nextComponentConfig.components || {}).length,
772
+ copiedSourceFiles: copiedSourceFiles.length
452
773
  }
453
774
  };
454
775
  }
@@ -461,7 +782,10 @@ module.exports = {
461
782
  deepMerge,
462
783
  mergeBindings,
463
784
  mergeScenes,
785
+ buildWorkflowBindings,
786
+ buildWorkflowResource,
464
787
  normalizeMcpServer,
465
788
  sanitizeResourceConfig,
466
- syncSddAgentToFlowMind
789
+ syncSddAgentToFlowMind,
790
+ autoSyncSddAgentToFlowMind
467
791
  };