flowmind 1.5.2 → 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) {
@@ -204,7 +210,81 @@ function buildWorkflowResource(skillBindingData = {}) {
204
210
  };
205
211
  }
206
212
 
207
- function buildWorkflowBindings(skillBindingData = {}) {
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) {
208
288
  const workflowBinding = skillBindingData.bindings?.['auto-flow'];
209
289
  if (!workflowBinding) return [];
210
290
 
@@ -249,7 +329,11 @@ function buildWorkflowBindings(skillBindingData = {}) {
249
329
  learningCount: workflowBinding.learningCount || 0,
250
330
  lastLearning: workflowBinding.lastLearning || null,
251
331
  records: workflowBinding.records || [],
252
- rules: workflowBinding.rules || []
332
+ rules: workflowBinding.rules || [],
333
+ transport: workflowTransport ? {
334
+ url: workflowTransport.url,
335
+ type: workflowTransport.type
336
+ } : undefined
253
337
  },
254
338
  metadata: {
255
339
  source: 'sdd-agent',
@@ -263,7 +347,7 @@ function buildWorkflowBindings(skillBindingData = {}) {
263
347
  }];
264
348
  }
265
349
 
266
- function convertResourceBindings(sddConfig, skillBindingData = {}) {
350
+ function convertResourceBindings(sddConfig, skillBindingData = {}, workflowTransport = null) {
267
351
  const resources = sddConfig.resources || {};
268
352
  const aliasIndex = buildAliasIndex(sddConfig.aliases);
269
353
  const bindings = [];
@@ -281,7 +365,7 @@ function convertResourceBindings(sddConfig, skillBindingData = {}) {
281
365
  }
282
366
  }
283
367
 
284
- bindings.push(...buildWorkflowBindings(skillBindingData));
368
+ bindings.push(...buildWorkflowBindings(skillBindingData, workflowTransport));
285
369
 
286
370
  return bindings;
287
371
  }
@@ -317,10 +401,13 @@ function convertSceneMappings(sceneData = {}) {
317
401
  }));
318
402
  }
319
403
 
320
- function buildComponentConfig(sddConfig, skillBindingData = {}) {
404
+ function buildComponentConfig(sddConfig, skillBindingData = {}, workflowTransport = null) {
321
405
  const resources = sddConfig.resources || {};
322
406
  const workflowResource = resources.workflow || buildWorkflowResource(skillBindingData);
323
407
  const components = {};
408
+ const directQueryServer = resources.database?.mcpServers?.directQuery
409
+ || resources.redis?.mcpServers?.directQuery
410
+ || null;
324
411
 
325
412
  if (resources.sls?.enabled) {
326
413
  const defaultEnv = resources.sls.defaultEnv || 'uat';
@@ -356,6 +443,18 @@ function buildComponentConfig(sddConfig, skillBindingData = {}) {
356
443
  };
357
444
  }
358
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
+
359
458
  if (resources.redis?.enabled && resources.redis.mcpServers?.monitor) {
360
459
  components.redisMonitor = {
361
460
  default: 'aliyun-redis',
@@ -398,7 +497,8 @@ function buildComponentConfig(sddConfig, skillBindingData = {}) {
398
497
  providers: {
399
498
  'friday-flow': {
400
499
  enabled: true,
401
- mcpServer: normalizeMcpServer(workflowResource.mcpServer)
500
+ mcpServer: normalizeMcpServer(workflowResource.mcpServer),
501
+ ...(workflowTransport ? { transport: workflowTransport } : {})
402
502
  }
403
503
  }
404
504
  };
@@ -483,6 +583,118 @@ async function readJsonIfExists(filePath, fallback) {
483
583
  return fs.readJson(filePath);
484
584
  }
485
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
+
486
698
  async function syncSddAgentToFlowMind(options = {}) {
487
699
  const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
488
700
  const targetHome = options.targetHome || getHomeDir();
@@ -500,11 +712,19 @@ async function syncSddAgentToFlowMind(options = {}) {
500
712
  const sddScenes = await readJsonIfExists(sourceScenesPath, { mappings: [] });
501
713
  const sddSkillBindings = await readJsonIfExists(sourceSkillBindingsPath, { version: '1.0', bindings: {} });
502
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
+ }
503
722
 
504
723
  const targetResourceConfigPath = path.join(targetDir, 'resource-config.json');
505
724
  const targetComponentConfigPath = path.join(targetDir, 'component-config.json');
506
725
  const targetBindingsPath = path.join(targetLearningDir, 'resource-bindings.json');
507
726
  const targetScenesPath = path.join(targetLearningDir, 'scenes.json');
727
+ const targetSourceDir = path.join(targetDir, 'source');
508
728
 
509
729
  const existingResourceConfig = await readJsonIfExists(targetResourceConfigPath, {});
510
730
  const existingComponentConfig = await readJsonIfExists(targetComponentConfigPath, { version: '1.0.0', components: {} });
@@ -512,8 +732,8 @@ async function syncSddAgentToFlowMind(options = {}) {
512
732
  const existingScenes = await readJsonIfExists(targetScenesPath, { version: '1.0', mappings: [] });
513
733
 
514
734
  const nextResourceConfig = deepMerge(existingResourceConfig, sanitizeResourceConfig(sddConfig, sddSkillBindings));
515
- const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig, sddSkillBindings));
516
- const importedBindings = convertResourceBindings(sddConfig, sddSkillBindings);
735
+ const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig, sddSkillBindings, workflowTransport));
736
+ const importedBindings = convertResourceBindings(sddConfig, sddSkillBindings, workflowTransport);
517
737
  const nextBindings = {
518
738
  version: existingBindings.version || '1.0',
519
739
  lastUpdated: new Date().toISOString(),
@@ -531,6 +751,7 @@ async function syncSddAgentToFlowMind(options = {}) {
531
751
  await fs.writeJson(targetComponentConfigPath, nextComponentConfig, { spaces: 2 });
532
752
  await fs.writeJson(targetBindingsPath, nextBindings, { spaces: 2 });
533
753
  await fs.writeJson(targetScenesPath, nextScenes, { spaces: 2 });
754
+ const copiedSourceFiles = await copySelectedSourceFiles(sourceDir, targetSourceDir);
534
755
 
535
756
  return {
536
757
  sourceDir,
@@ -539,14 +760,16 @@ async function syncSddAgentToFlowMind(options = {}) {
539
760
  resourceConfig: targetResourceConfigPath,
540
761
  componentConfig: targetComponentConfigPath,
541
762
  resourceBindings: targetBindingsPath,
542
- scenes: targetScenesPath
763
+ scenes: targetScenesPath,
764
+ sourceFiles: copiedSourceFiles.map((item) => item.targetPath)
543
765
  },
544
766
  counts: {
545
767
  importedBindings: importedBindings.length,
546
768
  totalBindings: nextBindings.bindings.length,
547
769
  importedScenes: importedScenes.length,
548
770
  totalScenes: nextScenes.mappings.length,
549
- components: Object.keys(nextComponentConfig.components || {}).length
771
+ components: Object.keys(nextComponentConfig.components || {}).length,
772
+ copiedSourceFiles: copiedSourceFiles.length
550
773
  }
551
774
  };
552
775
  }
@@ -563,5 +786,6 @@ module.exports = {
563
786
  buildWorkflowResource,
564
787
  normalizeMcpServer,
565
788
  sanitizeResourceConfig,
566
- syncSddAgentToFlowMind
789
+ syncSddAgentToFlowMind,
790
+ autoSyncSddAgentToFlowMind
567
791
  };
@@ -0,0 +1,324 @@
1
+ const fs = require('fs-extra');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const ENV_PATTERNS = Object.freeze([
6
+ { env: 'prod', pattern: /\bprod\b|生产/iu },
7
+ { env: 'gray', pattern: /\bgray\b|灰度/iu },
8
+ { env: 'uat', pattern: /\buat\b|预发/iu },
9
+ { env: 'test', pattern: /\btest\b|测试/iu },
10
+ { env: 'dev', pattern: /\bdev\b|开发/iu }
11
+ ]);
12
+
13
+ const DEFAULT_SCORE_LIMIT = 5;
14
+
15
+ let cachedSignature = null;
16
+ let cachedContext = null;
17
+
18
+ function getFlowMindHome() {
19
+ return process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
20
+ }
21
+
22
+ function getSourceDir(options = {}) {
23
+ if (options.sourceDir) {
24
+ return options.sourceDir;
25
+ }
26
+ const flowmindHome = options.flowmindHome || getFlowMindHome();
27
+ return path.join(flowmindHome, '.flowmind', 'source');
28
+ }
29
+
30
+ async function readJsonIfExists(filePath, fallback) {
31
+ if (!(await fs.pathExists(filePath))) {
32
+ return fallback;
33
+ }
34
+ return fs.readJson(filePath);
35
+ }
36
+
37
+ async function buildSignature(sourceDir) {
38
+ const files = [
39
+ 'RESOURCE_INDEX.md',
40
+ 'project-db-configs.json',
41
+ 'project-git-map.json'
42
+ ];
43
+ const entries = [];
44
+ for (const fileName of files) {
45
+ const filePath = path.join(sourceDir, fileName);
46
+ try {
47
+ const stat = await fs.stat(filePath);
48
+ entries.push(`${fileName}:${stat.size}:${stat.mtimeMs}`);
49
+ } catch (error) {
50
+ entries.push(`${fileName}:missing`);
51
+ }
52
+ }
53
+ return entries.join('|');
54
+ }
55
+
56
+ function parseList(value) {
57
+ if (!value) return [];
58
+ return String(value)
59
+ .split(',')
60
+ .map((item) => item.trim())
61
+ .filter(Boolean);
62
+ }
63
+
64
+ function parseResourceIndex(markdown = '') {
65
+ const lines = String(markdown).split(/\r?\n/);
66
+ const entries = [];
67
+ let current = null;
68
+
69
+ for (const rawLine of lines) {
70
+ const line = rawLine.trim();
71
+ if (!line) continue;
72
+
73
+ if (line.startsWith('### ')) {
74
+ current = {
75
+ title: line.slice(4).trim(),
76
+ path: null,
77
+ aliases: [],
78
+ domains: [],
79
+ modules: [],
80
+ tags: [],
81
+ note: null
82
+ };
83
+ entries.push(current);
84
+ continue;
85
+ }
86
+
87
+ if (!current || !line.startsWith('- ')) {
88
+ continue;
89
+ }
90
+
91
+ const separatorIndex = line.indexOf(':');
92
+ if (separatorIndex < 0) continue;
93
+
94
+ const key = line.slice(2, separatorIndex).trim();
95
+ const value = line.slice(separatorIndex + 1).trim();
96
+
97
+ if (key === 'path') current.path = value;
98
+ if (key === 'aliases') current.aliases = parseList(value);
99
+ if (key === 'domains') current.domains = parseList(value);
100
+ if (key === 'modules') current.modules = parseList(value);
101
+ if (key === 'tags') current.tags = parseList(value);
102
+ if (key === 'note') current.note = value;
103
+ }
104
+
105
+ return entries;
106
+ }
107
+
108
+ function extractKeywords(input) {
109
+ return (String(input || '').match(/[\u4e00-\u9fa5A-Za-z0-9._-]+/g) || [])
110
+ .map((item) => item.toLowerCase());
111
+ }
112
+
113
+ function detectEnvironment(input) {
114
+ const normalized = String(input || '').toLowerCase();
115
+ const matched = ENV_PATTERNS.find(({ pattern }) => pattern.test(normalized));
116
+ return matched ? matched.env : null;
117
+ }
118
+
119
+ function normalizeProjectService(service = {}) {
120
+ return {
121
+ ...service,
122
+ databaseId: service.databaseId || service.dmsDatabaseId || null,
123
+ sourceId: service.sourceId || null
124
+ };
125
+ }
126
+
127
+ function buildProjectCatalog(projectConfigs = {}, gitMap = {}, resourceIndex = []) {
128
+ const catalog = new Map();
129
+ const serverProjects = gitMap.serverProjects || {};
130
+ const mobileProjects = gitMap.mobileProjects || {};
131
+
132
+ const ensureProject = (projectName) => {
133
+ if (!projectName) return null;
134
+ if (!catalog.has(projectName)) {
135
+ catalog.set(projectName, {
136
+ name: projectName,
137
+ config: projectConfigs[projectName] || null,
138
+ git: serverProjects[projectName] || mobileProjects[projectName] || null,
139
+ resourceEntries: []
140
+ });
141
+ }
142
+ return catalog.get(projectName);
143
+ };
144
+
145
+ for (const projectName of Object.keys(projectConfigs)) {
146
+ ensureProject(projectName);
147
+ }
148
+
149
+ for (const projectName of Object.keys(serverProjects)) {
150
+ ensureProject(projectName);
151
+ }
152
+
153
+ for (const projectName of Object.keys(mobileProjects)) {
154
+ ensureProject(projectName);
155
+ }
156
+
157
+ for (const entry of resourceIndex) {
158
+ for (const moduleName of entry.modules || []) {
159
+ const project = ensureProject(moduleName);
160
+ if (project) {
161
+ project.resourceEntries.push(entry);
162
+ }
163
+ }
164
+ }
165
+
166
+ return Array.from(catalog.values());
167
+ }
168
+
169
+ function scoreMatch(normalizedInput, value, score, reason) {
170
+ if (!value) return null;
171
+ const normalizedValue = String(value).toLowerCase();
172
+ if (!normalizedValue) return null;
173
+ if (!normalizedInput.includes(normalizedValue)) return null;
174
+ return { score, reason, match: value };
175
+ }
176
+
177
+ function scoreProjectCandidate(project, normalizedInput, keywords = []) {
178
+ let score = 0;
179
+ const reasons = [];
180
+
181
+ const directMatches = [
182
+ scoreMatch(normalizedInput, project.name, 18, 'project-name'),
183
+ scoreMatch(normalizedInput, stripEnvPrefix(project.name), 14, 'project-name-base'),
184
+ scoreMatch(normalizedInput, project.git?.description, 6, 'git-description')
185
+ ].filter(Boolean);
186
+
187
+ for (const match of directMatches) {
188
+ score += match.score;
189
+ reasons.push(match);
190
+ }
191
+
192
+ for (const entry of project.resourceEntries || []) {
193
+ const entryMatches = [
194
+ scoreMatch(normalizedInput, entry.title, 8, 'resource-title'),
195
+ ...entry.aliases.map((value) => scoreMatch(normalizedInput, value, 8, 'resource-alias')),
196
+ ...entry.modules.map((value) => scoreMatch(normalizedInput, value, 10, 'resource-module')),
197
+ ...entry.domains.map((value) => scoreMatch(normalizedInput, value, 3, 'resource-domain')),
198
+ ...entry.tags.map((value) => scoreMatch(normalizedInput, value, 2, 'resource-tag'))
199
+ ].filter(Boolean);
200
+
201
+ for (const match of entryMatches) {
202
+ score += match.score;
203
+ reasons.push({ ...match, entry: entry.title });
204
+ }
205
+ }
206
+
207
+ const description = String(project.git?.description || '').toLowerCase();
208
+ if (description) {
209
+ for (const keyword of keywords) {
210
+ if (keyword.length < 2) continue;
211
+ if (description.includes(keyword)) {
212
+ score += 1;
213
+ reasons.push({ score: 1, reason: 'git-description-keyword', match: keyword });
214
+ }
215
+ }
216
+ }
217
+
218
+ return {
219
+ name: project.name,
220
+ score,
221
+ reasons,
222
+ config: project.config,
223
+ git: project.git,
224
+ resourceEntries: project.resourceEntries || []
225
+ };
226
+ }
227
+
228
+ function stripEnvPrefix(projectName) {
229
+ return String(projectName || '').replace(/^(?:test|dev|uat|gray|prod)-/i, '');
230
+ }
231
+
232
+ async function loadSourceContext(options = {}) {
233
+ const sourceDir = getSourceDir(options);
234
+ const signature = await buildSignature(sourceDir);
235
+
236
+ if (cachedContext && cachedSignature === `${sourceDir}:${signature}`) {
237
+ return cachedContext;
238
+ }
239
+
240
+ const [projectDbConfigs, projectGitMap, resourceIndexContent] = await Promise.all([
241
+ readJsonIfExists(path.join(sourceDir, 'project-db-configs.json'), { configs: {} }),
242
+ readJsonIfExists(path.join(sourceDir, 'project-git-map.json'), { serverProjects: {}, mobileProjects: {} }),
243
+ fs.pathExists(path.join(sourceDir, 'RESOURCE_INDEX.md'))
244
+ ? fs.readFile(path.join(sourceDir, 'RESOURCE_INDEX.md'), 'utf8')
245
+ : Promise.resolve('')
246
+ ]);
247
+
248
+ const resourceIndex = parseResourceIndex(resourceIndexContent);
249
+ const projectConfigs = projectDbConfigs.configs || {};
250
+ const projectCatalog = buildProjectCatalog(projectConfigs, projectGitMap, resourceIndex);
251
+
252
+ cachedContext = {
253
+ sourceDir,
254
+ projectConfigs,
255
+ projectGitMap,
256
+ resourceIndex,
257
+ projectCatalog
258
+ };
259
+ cachedSignature = `${sourceDir}:${signature}`;
260
+ return cachedContext;
261
+ }
262
+
263
+ async function inferSourceContext(input, options = {}) {
264
+ const sourceContext = await loadSourceContext(options);
265
+ const normalizedInput = String(input || '').toLowerCase();
266
+ const keywords = extractKeywords(input);
267
+ const explicitEnv = detectEnvironment(input);
268
+ const candidates = sourceContext.projectCatalog
269
+ .map((project) => scoreProjectCandidate(project, normalizedInput, keywords))
270
+ .filter((candidate) => candidate.score > 0)
271
+ .sort((left, right) => right.score - left.score);
272
+
273
+ const topCandidates = candidates.slice(0, options.limit || DEFAULT_SCORE_LIMIT);
274
+ const selected = topCandidates[0] || null;
275
+
276
+ if (!selected) {
277
+ return {
278
+ matched: false,
279
+ sourceDir: sourceContext.sourceDir,
280
+ environment: explicitEnv,
281
+ candidates: []
282
+ };
283
+ }
284
+
285
+ const config = selected.config || {};
286
+ const mysql = config.services?.mysql ? normalizeProjectService(config.services.mysql) : null;
287
+ const redis = config.services?.redis ? normalizeProjectService(config.services.redis) : null;
288
+ const environment = explicitEnv
289
+ || config.environment
290
+ || mysql?.envType
291
+ || redis?.envType
292
+ || null;
293
+
294
+ return {
295
+ matched: true,
296
+ sourceDir: sourceContext.sourceDir,
297
+ project: selected.name,
298
+ environment,
299
+ gitProject: selected.git || null,
300
+ database: mysql,
301
+ redis,
302
+ cacheKeys: config.cacheKeys || {},
303
+ config,
304
+ matchedEntries: selected.resourceEntries.slice(0, 3).map((entry) => ({
305
+ title: entry.title,
306
+ aliases: entry.aliases,
307
+ modules: entry.modules,
308
+ tags: entry.tags
309
+ })),
310
+ candidates: topCandidates.map((candidate) => ({
311
+ project: candidate.name,
312
+ score: candidate.score,
313
+ reasons: candidate.reasons.slice(0, 8)
314
+ }))
315
+ };
316
+ }
317
+
318
+ module.exports = {
319
+ detectEnvironment,
320
+ getSourceDir,
321
+ inferSourceContext,
322
+ loadSourceContext,
323
+ parseResourceIndex
324
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Memory and workflow automation for MCP, Codex, and Claude Code. Reuse repeatable developer operations through skills and explicit feedback.",
5
5
  "main": "core/index.js",
6
6
  "bin": {