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.
@@ -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.1",
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": {