@mrxkun/mcfast-mcp 4.1.10 → 4.1.12
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/package.json +2 -2
- package/src/index.js +297 -21
- package/src/memory/memory-engine.js +232 -25
- package/src/memory/stores/base-database.js +223 -0
- package/src/memory/utils/chunker.js +1 -0
- package/src/memory/utils/indexer.js +110 -4
- package/src/memory/utils/logger.js +162 -0
- package/src/memory/utils/vector-index.js +241 -0
- package/src/memory/watchers/file-watcher.js +257 -103
- package/src/tools/project_analyze.js +491 -0
- package/src/utils/audit-queue.js +1 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* File Watcher
|
|
3
3
|
* Theo dõi thay đổi file và index tự động
|
|
4
4
|
* Debounced 1.5s để batch rapid changes
|
|
5
|
+
*
|
|
6
|
+
* IMPROVEMENTS v4.1.10:
|
|
7
|
+
* - Added periodic cleanup for pendingUpdates Map
|
|
8
|
+
* - Added error boundaries for async operations
|
|
9
|
+
* - Added graceful shutdown cleanup
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
import chokidar from 'chokidar';
|
|
@@ -29,9 +34,13 @@ export class FileWatcher {
|
|
|
29
34
|
this.watcher = null;
|
|
30
35
|
this.pendingUpdates = new Map();
|
|
31
36
|
this.isProcessing = false;
|
|
37
|
+
this.cleanupInterval = null;
|
|
38
|
+
this.isRunning = false;
|
|
32
39
|
|
|
33
40
|
// Configuration
|
|
34
41
|
this.debounceMs = options.debounceMs || 1500;
|
|
42
|
+
this.cleanupIntervalMs = options.cleanupIntervalMs || 30000; // Cleanup every 30s
|
|
43
|
+
this.maxPendingUpdates = options.maxPendingUpdates || 1000;
|
|
35
44
|
this.ignored = options.ignored || [
|
|
36
45
|
'**/node_modules/**',
|
|
37
46
|
'**/.git/**',
|
|
@@ -46,40 +55,100 @@ export class FileWatcher {
|
|
|
46
55
|
filesAdded: 0,
|
|
47
56
|
filesChanged: 0,
|
|
48
57
|
filesDeleted: 0,
|
|
49
|
-
errors: 0
|
|
58
|
+
errors: 0,
|
|
59
|
+
totalProcessed: 0
|
|
50
60
|
};
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
async start() {
|
|
64
|
+
if (this.isRunning) {
|
|
65
|
+
console.error('[FileWatcher] Already running');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
console.error(`[FileWatcher] Starting watcher for: ${this.projectPath}`);
|
|
55
70
|
console.error(`[FileWatcher] Debounce: ${this.debounceMs}ms`);
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
try {
|
|
73
|
+
this.watcher = chokidar.watch(this.projectPath, {
|
|
74
|
+
ignored: this.ignored,
|
|
75
|
+
persistent: true,
|
|
76
|
+
ignoreInitial: true,
|
|
77
|
+
awaitWriteFinish: {
|
|
78
|
+
stabilityThreshold: 300,
|
|
79
|
+
pollInterval: 100
|
|
80
|
+
}
|
|
81
|
+
});
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
// Bind event handlers with error handling
|
|
84
|
+
this.watcher.on('add', filePath => this.safeHandleAdd(filePath));
|
|
85
|
+
this.watcher.on('change', filePath => this.safeHandleChange(filePath));
|
|
86
|
+
this.watcher.on('unlink', filePath => this.safeHandleDelete(filePath));
|
|
87
|
+
this.watcher.on('error', error => this.handleError(error));
|
|
88
|
+
|
|
89
|
+
// Setup debounced flush
|
|
90
|
+
this.flushQueue = debounce(() => this.processQueue(), this.debounceMs);
|
|
91
|
+
|
|
92
|
+
// Wait for ready
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
this.watcher.once('ready', resolve);
|
|
95
|
+
this.watcher.once('error', reject);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Start periodic cleanup
|
|
99
|
+
this.startCleanup();
|
|
100
|
+
|
|
101
|
+
this.isRunning = true;
|
|
102
|
+
console.error(`[FileWatcher] Ready and watching`);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`[FileWatcher] Failed to start:`, error.message);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Safe handlers with try-catch
|
|
110
|
+
safeHandleAdd(filePath) {
|
|
111
|
+
try {
|
|
112
|
+
this.handleAdd(filePath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`[FileWatcher] Error in handleAdd:`, error.message);
|
|
115
|
+
this.stats.errors++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
safeHandleChange(filePath) {
|
|
120
|
+
try {
|
|
121
|
+
this.handleChange(filePath);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`[FileWatcher] Error in handleChange:`, error.message);
|
|
124
|
+
this.stats.errors++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
safeHandleDelete(filePath) {
|
|
129
|
+
try {
|
|
130
|
+
this.handleDelete(filePath);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`[FileWatcher] Error in handleDelete:`, error.message);
|
|
133
|
+
this.stats.errors++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Periodic cleanup to prevent memory leak
|
|
138
|
+
startCleanup() {
|
|
139
|
+
if (this.cleanupInterval) {
|
|
140
|
+
clearInterval(this.cleanupInterval);
|
|
141
|
+
}
|
|
75
142
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
this.cleanupInterval = setInterval(() => {
|
|
144
|
+
if (this.pendingUpdates.size > this.maxPendingUpdates) {
|
|
145
|
+
console.error(`[FileWatcher] Cleanup: clearing ${this.pendingUpdates.size} pending updates`);
|
|
146
|
+
this.pendingUpdates.clear();
|
|
147
|
+
}
|
|
148
|
+
}, this.cleanupIntervalMs);
|
|
81
149
|
|
|
82
|
-
|
|
150
|
+
// Prevent interval from keeping process alive
|
|
151
|
+
this.cleanupInterval.unref();
|
|
83
152
|
}
|
|
84
153
|
|
|
85
154
|
handleAdd(filePath) {
|
|
@@ -119,7 +188,6 @@ export class FileWatcher {
|
|
|
119
188
|
|
|
120
189
|
async processQueue() {
|
|
121
190
|
if (this.isProcessing) {
|
|
122
|
-
// If already processing, debounce will call again
|
|
123
191
|
return;
|
|
124
192
|
}
|
|
125
193
|
|
|
@@ -139,14 +207,25 @@ export class FileWatcher {
|
|
|
139
207
|
|
|
140
208
|
// Process deletions first
|
|
141
209
|
for (const update of deletes) {
|
|
142
|
-
|
|
210
|
+
try {
|
|
211
|
+
await this.processDelete(update);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`[FileWatcher] Error processing delete:`, error.message);
|
|
214
|
+
this.stats.errors++;
|
|
215
|
+
}
|
|
143
216
|
}
|
|
144
217
|
|
|
145
218
|
// Process additions/changes
|
|
146
219
|
for (const update of adds) {
|
|
147
|
-
|
|
220
|
+
try {
|
|
221
|
+
await this.processFile(update);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`[FileWatcher] Error processing file:`, error.message);
|
|
224
|
+
this.stats.errors++;
|
|
225
|
+
}
|
|
148
226
|
}
|
|
149
227
|
|
|
228
|
+
this.stats.totalProcessed += updates.length;
|
|
150
229
|
console.error(`[FileWatcher] Processed ${updates.length} updates`);
|
|
151
230
|
|
|
152
231
|
} catch (error) {
|
|
@@ -165,19 +244,19 @@ export class FileWatcher {
|
|
|
165
244
|
const stats = await fs.stat(filePath).catch(() => null);
|
|
166
245
|
if (!stats || !stats.isFile()) return;
|
|
167
246
|
|
|
168
|
-
//
|
|
169
|
-
if (stats.size > 1024 * 1024) {
|
|
170
|
-
console.error(`[FileWatcher] Skipping large file: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Read file content
|
|
247
|
+
// Read file content first
|
|
175
248
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
176
249
|
const contentHash = crypto.createHash('md5').update(content).digest('hex');
|
|
177
250
|
|
|
251
|
+
// Handle large files (> 1MB)
|
|
252
|
+
if (stats.size > 1024 * 1024) {
|
|
253
|
+
console.error(`[FileWatcher] Large file detected: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
|
|
254
|
+
return this.indexLargeFile(filePath, content, contentHash, stats);
|
|
255
|
+
}
|
|
256
|
+
|
|
178
257
|
// Check if already indexed with same hash
|
|
179
|
-
|
|
180
|
-
|
|
258
|
+
const existingFile = this.memory.codebaseDb?.getFileByPath?.(filePath);
|
|
259
|
+
if (existingFile && existingFile.content_hash === contentHash) {
|
|
181
260
|
return;
|
|
182
261
|
}
|
|
183
262
|
|
|
@@ -222,77 +301,102 @@ export class FileWatcher {
|
|
|
222
301
|
async indexMarkdownFile(filePath, content, contentHash, stats) {
|
|
223
302
|
console.error(`[FileWatcher] Indexing Markdown: ${filePath}`);
|
|
224
303
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Chunk the content
|
|
230
|
-
const { MarkdownChunker } = await import('../utils/markdown-chunker.js');
|
|
231
|
-
const chunker = new MarkdownChunker();
|
|
232
|
-
const chunks = chunker.chunk(content, relativePath);
|
|
233
|
-
|
|
234
|
-
// Track file
|
|
235
|
-
this.memory.memoryDb?.upsertFile?.(relativePath, contentHash, stats.mtimeMs, stats.size);
|
|
236
|
-
|
|
237
|
-
// Generate embeddings and insert chunks
|
|
238
|
-
for (const chunk of chunks) {
|
|
239
|
-
// Check cache first
|
|
240
|
-
let embedding = null;
|
|
241
|
-
const cached = this.memory.memoryDb?.getCachedEmbedding?.(chunk.contentHash);
|
|
304
|
+
try {
|
|
305
|
+
// Delete old chunks if updating
|
|
306
|
+
const relativePath = path.relative(this.projectPath, filePath);
|
|
307
|
+
this.memory.memoryDb?.deleteChunksByFile?.(relativePath);
|
|
242
308
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
embedding = await this.memory.embedder?.embedCode?.(chunk.content);
|
|
248
|
-
|
|
249
|
-
// Cache it
|
|
250
|
-
if (embedding) {
|
|
251
|
-
this.memory.memoryDb?.cacheEmbedding?.(
|
|
252
|
-
chunk.contentHash,
|
|
253
|
-
Buffer.from(new Float32Array(embedding).buffer),
|
|
254
|
-
'simple-embedder',
|
|
255
|
-
embedding.length
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
309
|
+
// Chunk the content
|
|
310
|
+
const { MarkdownChunker } = await import('../utils/markdown-chunker.js');
|
|
311
|
+
const chunker = new MarkdownChunker();
|
|
312
|
+
const chunks = chunker.chunk(content, relativePath);
|
|
259
313
|
|
|
260
|
-
//
|
|
261
|
-
this.memory.memoryDb?.
|
|
262
|
-
id: chunk.id,
|
|
263
|
-
file_path: chunk.filePath,
|
|
264
|
-
start_line: chunk.startLine,
|
|
265
|
-
end_line: chunk.endLine,
|
|
266
|
-
content: chunk.content,
|
|
267
|
-
content_hash: chunk.contentHash,
|
|
268
|
-
chunk_type: chunk.chunkType
|
|
269
|
-
});
|
|
314
|
+
// Track file
|
|
315
|
+
this.memory.memoryDb?.upsertFile?.(relativePath, contentHash, stats.mtimeMs, stats.size);
|
|
270
316
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
embedding
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
317
|
+
// Generate embeddings and insert chunks
|
|
318
|
+
for (const chunk of chunks) {
|
|
319
|
+
try {
|
|
320
|
+
// Check cache first
|
|
321
|
+
let embedding = null;
|
|
322
|
+
const cached = this.memory.memoryDb?.getCachedEmbedding?.(chunk.contentHash);
|
|
323
|
+
|
|
324
|
+
if (cached) {
|
|
325
|
+
embedding = cached.embedding;
|
|
326
|
+
} else {
|
|
327
|
+
// Generate embedding
|
|
328
|
+
embedding = await this.memory.embedder?.embedCode?.(chunk.content);
|
|
329
|
+
|
|
330
|
+
// Cache it
|
|
331
|
+
if (embedding) {
|
|
332
|
+
this.memory.memoryDb?.cacheEmbedding?.(
|
|
333
|
+
chunk.contentHash,
|
|
334
|
+
Buffer.from(new Float32Array(embedding).buffer),
|
|
335
|
+
'simple-embedder',
|
|
336
|
+
embedding.length
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Insert chunk
|
|
342
|
+
this.memory.memoryDb?.insertChunk?.({
|
|
343
|
+
id: chunk.id,
|
|
344
|
+
file_path: chunk.filePath,
|
|
345
|
+
start_line: chunk.startLine,
|
|
346
|
+
end_line: chunk.endLine,
|
|
347
|
+
content: chunk.content,
|
|
348
|
+
content_hash: chunk.contentHash,
|
|
349
|
+
chunk_type: chunk.chunkType
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Insert embedding
|
|
353
|
+
if (embedding) {
|
|
354
|
+
this.memory.memoryDb?.insertEmbedding?.({
|
|
355
|
+
chunk_id: chunk.id,
|
|
356
|
+
embedding: Buffer.from(new Float32Array(embedding).buffer),
|
|
357
|
+
model: 'simple-embedder',
|
|
358
|
+
dimensions: embedding.length
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
} catch (chunkError) {
|
|
362
|
+
console.error(`[FileWatcher] Error indexing chunk:`, chunkError.message);
|
|
363
|
+
}
|
|
279
364
|
}
|
|
365
|
+
|
|
366
|
+
console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error(`[FileWatcher] Error indexing markdown ${filePath}:`, error.message);
|
|
369
|
+
this.stats.errors++;
|
|
280
370
|
}
|
|
281
|
-
|
|
282
|
-
console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
|
|
283
371
|
}
|
|
284
372
|
|
|
285
|
-
async indexCodeFile(filePath, content, contentHash, stats) {
|
|
373
|
+
async indexCodeFile(filePath, content, contentHash, stats, maxRetries = 3) {
|
|
286
374
|
console.error(`[FileWatcher] Indexing code: ${filePath}`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
375
|
+
|
|
376
|
+
if (!this.memory.indexer) {
|
|
377
|
+
console.error(`[FileWatcher] No indexer available`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
290
382
|
try {
|
|
291
383
|
const indexed = await this.memory.indexer.indexFile(filePath, content);
|
|
292
|
-
await this.memory.storeIndexed(indexed);
|
|
293
|
-
|
|
384
|
+
const stored = await this.memory.storeIndexed(indexed);
|
|
385
|
+
|
|
386
|
+
if (stored) {
|
|
387
|
+
console.error(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks`);
|
|
388
|
+
return;
|
|
389
|
+
} else {
|
|
390
|
+
throw new Error('storeIndexed returned false');
|
|
391
|
+
}
|
|
294
392
|
} catch (error) {
|
|
295
|
-
|
|
393
|
+
if (attempt === maxRetries) {
|
|
394
|
+
console.error(`[FileWatcher] Failed to index after ${maxRetries} attempts:`, error.message);
|
|
395
|
+
this.stats.errors++;
|
|
396
|
+
} else {
|
|
397
|
+
console.warn(`[FileWatcher] Retry ${attempt}/${maxRetries}:`, error.message);
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
|
|
399
|
+
}
|
|
296
400
|
}
|
|
297
401
|
}
|
|
298
402
|
}
|
|
@@ -302,25 +406,75 @@ export class FileWatcher {
|
|
|
302
406
|
return ext === '.md' || ext === '.markdown';
|
|
303
407
|
}
|
|
304
408
|
|
|
409
|
+
async indexLargeFile(filePath, content, contentHash, stats) {
|
|
410
|
+
const chunkSize = 500 * 1024;
|
|
411
|
+
try {
|
|
412
|
+
const lines = content.split('\n');
|
|
413
|
+
let chunkContent = '';
|
|
414
|
+
let chunkIndex = 0;
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
417
|
+
chunkContent += lines[i] + '\n';
|
|
418
|
+
|
|
419
|
+
if (chunkContent.length >= chunkSize || i === lines.length - 1) {
|
|
420
|
+
console.error(`[FileWatcher] Indexing chunk ${chunkIndex + 1} of large file`);
|
|
421
|
+
|
|
422
|
+
if (this.isMarkdownFile(filePath)) {
|
|
423
|
+
await this.indexMarkdownFile(filePath, chunkContent, contentHash + '_' + chunkIndex, stats);
|
|
424
|
+
} else {
|
|
425
|
+
await this.indexCodeFile(filePath, chunkContent, contentHash + '_' + chunkIndex, stats);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
chunkIndex++;
|
|
429
|
+
chunkContent = '';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.error(`[FileWatcher] Large file indexed in ${chunkIndex} chunks`);
|
|
434
|
+
this.stats.filesChanged++;
|
|
435
|
+
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error(`[FileWatcher] Failed to index large file:`, error.message);
|
|
438
|
+
this.stats.errors++;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
305
442
|
getStats() {
|
|
306
443
|
return {
|
|
307
444
|
...this.stats,
|
|
308
445
|
pendingUpdates: this.pendingUpdates.size,
|
|
309
|
-
isProcessing: this.isProcessing
|
|
446
|
+
isProcessing: this.isProcessing,
|
|
447
|
+
isRunning: this.isRunning
|
|
310
448
|
};
|
|
311
449
|
}
|
|
312
450
|
|
|
313
451
|
async stop() {
|
|
452
|
+
if (!this.isRunning) return;
|
|
453
|
+
|
|
454
|
+
console.error('[FileWatcher] Stopping...');
|
|
455
|
+
|
|
456
|
+
// 1. Stop cleanup interval
|
|
457
|
+
if (this.cleanupInterval) {
|
|
458
|
+
clearInterval(this.cleanupInterval);
|
|
459
|
+
this.cleanupInterval = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 2. Clear pending updates map (free memory)
|
|
463
|
+
this.pendingUpdates.clear();
|
|
464
|
+
|
|
465
|
+
// 3. Cancel debounce timeout if exists
|
|
466
|
+
if (this.flushQueue && this.flushQueue.cancel) {
|
|
467
|
+
this.flushQueue.cancel();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 4. Close chokidar watcher (critical - prevents persistent file watching)
|
|
314
471
|
if (this.watcher) {
|
|
315
|
-
// Process any remaining updates
|
|
316
|
-
if (this.pendingUpdates.size > 0) {
|
|
317
|
-
await this.processQueue();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
472
|
await this.watcher.close();
|
|
321
473
|
this.watcher = null;
|
|
322
|
-
console.error(`[FileWatcher] Stopped`);
|
|
323
474
|
}
|
|
475
|
+
|
|
476
|
+
this.isRunning = false;
|
|
477
|
+
console.error('[FileWatcher] Stopped');
|
|
324
478
|
}
|
|
325
479
|
}
|
|
326
480
|
|