@mrxkun/mcfast-mcp 4.0.15 → 4.1.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.
@@ -1,6 +1,15 @@
1
+ /**
2
+ * File Watcher
3
+ * Theo dõi thay đổi file và index tự động
4
+ * Debounced 1.5s để batch rapid changes
5
+ */
6
+
1
7
  import chokidar from 'chokidar';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
2
11
 
3
- // Simple debounce implementation to avoid lodash dependency
12
+ // Simple debounce implementation
4
13
  function debounce(func, wait) {
5
14
  let timeout;
6
15
  return function executedFunction(...args) {
@@ -14,47 +23,304 @@ function debounce(func, wait) {
14
23
  }
15
24
 
16
25
  export class FileWatcher {
17
- constructor(projectPath, memoryEngine) {
26
+ constructor(projectPath, memoryEngine, options = {}) {
18
27
  this.projectPath = projectPath;
19
28
  this.memory = memoryEngine;
20
29
  this.watcher = null;
21
30
  this.pendingUpdates = new Map();
31
+ this.isProcessing = false;
32
+
33
+ // Configuration
34
+ this.debounceMs = options.debounceMs || 1500;
35
+ this.ignored = options.ignored || [
36
+ '**/node_modules/**',
37
+ '**/.git/**',
38
+ '**/dist/**',
39
+ '**/build/**',
40
+ '**/.mcfast/**',
41
+ '**/*.log'
42
+ ];
43
+
44
+ // Statistics
45
+ this.stats = {
46
+ filesAdded: 0,
47
+ filesChanged: 0,
48
+ filesDeleted: 0,
49
+ errors: 0
50
+ };
22
51
  }
23
52
 
24
53
  async start() {
54
+ console.log(`[FileWatcher] Starting watcher for: ${this.projectPath}`);
55
+ console.log(`[FileWatcher] Debounce: ${this.debounceMs}ms`);
56
+
25
57
  this.watcher = chokidar.watch(this.projectPath, {
26
- ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
27
- persistent: true
58
+ ignored: this.ignored,
59
+ persistent: true,
60
+ ignoreInitial: true, // Don't trigger on existing files
61
+ awaitWriteFinish: {
62
+ stabilityThreshold: 300,
63
+ pollInterval: 100
64
+ }
65
+ });
66
+
67
+ // Bind event handlers
68
+ this.watcher.on('add', filePath => this.handleAdd(filePath));
69
+ this.watcher.on('change', filePath => this.handleChange(filePath));
70
+ this.watcher.on('unlink', filePath => this.handleDelete(filePath));
71
+ this.watcher.on('error', error => this.handleError(error));
72
+
73
+ // Setup debounced flush
74
+ this.flushQueue = debounce(() => this.processQueue(), this.debounceMs);
75
+
76
+ // Wait for ready
77
+ await new Promise((resolve, reject) => {
78
+ this.watcher.once('ready', resolve);
79
+ this.watcher.once('error', reject);
80
+ });
81
+
82
+ console.log(`[FileWatcher] Ready and watching`);
83
+ }
84
+
85
+ handleAdd(filePath) {
86
+ console.log(`[FileWatcher] File added: ${filePath}`);
87
+ this.pendingUpdates.set(filePath, {
88
+ path: filePath,
89
+ type: 'add',
90
+ timestamp: Date.now()
91
+ });
92
+ this.flushQueue();
93
+ }
94
+
95
+ handleChange(filePath) {
96
+ console.log(`[FileWatcher] File changed: ${filePath}`);
97
+ this.pendingUpdates.set(filePath, {
98
+ path: filePath,
99
+ type: 'change',
100
+ timestamp: Date.now()
28
101
  });
102
+ this.flushQueue();
103
+ }
104
+
105
+ handleDelete(filePath) {
106
+ console.log(`[FileWatcher] File deleted: ${filePath}`);
107
+ this.pendingUpdates.set(filePath, {
108
+ path: filePath,
109
+ type: 'delete',
110
+ timestamp: Date.now()
111
+ });
112
+ this.flushQueue();
113
+ }
114
+
115
+ handleError(error) {
116
+ console.error(`[FileWatcher] Error:`, error);
117
+ this.stats.errors++;
118
+ }
29
119
 
30
- this.watcher.on('add', path => this.queueUpdate(path, 'add'));
31
- this.watcher.on('change', path => this.queueUpdate(path, 'change'));
32
- this.watcher.on('unlink', path => this.handleDelete(path));
120
+ async processQueue() {
121
+ if (this.isProcessing) {
122
+ // If already processing, debounce will call again
123
+ return;
124
+ }
125
+
126
+ this.isProcessing = true;
127
+
128
+ try {
129
+ const updates = Array.from(this.pendingUpdates.values());
130
+ this.pendingUpdates.clear();
131
+
132
+ if (updates.length === 0) return;
133
+
134
+ console.log(`[FileWatcher] Processing ${updates.length} updates...`);
135
+
136
+ // Group by type for efficiency
137
+ const adds = updates.filter(u => u.type === 'add' || u.type === 'change');
138
+ const deletes = updates.filter(u => u.type === 'delete');
139
+
140
+ // Process deletions first
141
+ for (const update of deletes) {
142
+ await this.processDelete(update);
143
+ }
144
+
145
+ // Process additions/changes
146
+ for (const update of adds) {
147
+ await this.processFile(update);
148
+ }
149
+
150
+ console.log(`[FileWatcher] Processed ${updates.length} updates`);
151
+
152
+ } catch (error) {
153
+ console.error(`[FileWatcher] Error processing queue:`, error);
154
+ this.stats.errors++;
155
+ } finally {
156
+ this.isProcessing = false;
157
+ }
158
+ }
33
159
 
34
- this.processQueue = debounce(() => this.flushQueue(), 1500);
160
+ async processFile(update) {
161
+ const filePath = update.path;
162
+
163
+ try {
164
+ // Check if file exists and is readable
165
+ const stats = await fs.stat(filePath).catch(() => null);
166
+ if (!stats || !stats.isFile()) return;
167
+
168
+ // Skip large files (> 1MB)
169
+ if (stats.size > 1024 * 1024) {
170
+ console.log(`[FileWatcher] Skipping large file: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
171
+ return;
172
+ }
173
+
174
+ // Read file content
175
+ const content = await fs.readFile(filePath, 'utf-8');
176
+ const contentHash = crypto.createHash('md5').update(content).digest('hex');
177
+
178
+ // Check if already indexed with same hash
179
+ if (this.memory.codebaseDb?.isFileIndexed?.(filePath, contentHash)) {
180
+ console.log(`[FileWatcher] File unchanged: ${filePath}`);
181
+ return;
182
+ }
183
+
184
+ // Determine file type and index accordingly
185
+ if (this.isMarkdownFile(filePath)) {
186
+ await this.indexMarkdownFile(filePath, content, contentHash, stats);
187
+ } else {
188
+ await this.indexCodeFile(filePath, content, contentHash, stats);
189
+ }
190
+
191
+ if (update.type === 'add') {
192
+ this.stats.filesAdded++;
193
+ } else {
194
+ this.stats.filesChanged++;
195
+ }
196
+
197
+ } catch (error) {
198
+ console.error(`[FileWatcher] Error processing ${filePath}:`, error.message);
199
+ this.stats.errors++;
200
+ }
35
201
  }
36
202
 
37
- queueUpdate(filePath, type) {
38
- this.pendingUpdates.set(filePath, { path: filePath, type, timestamp: Date.now() });
39
- this.processQueue();
203
+ async processDelete(update) {
204
+ const filePath = update.path;
205
+
206
+ try {
207
+ // Delete from codebase database
208
+ const file = this.memory.codebaseDb?.getFileByPath?.(filePath);
209
+ if (file) {
210
+ this.memory.codebaseDb.deleteFile(file.id);
211
+ console.log(`[FileWatcher] Deleted from index: ${filePath}`);
212
+ }
213
+
214
+ this.stats.filesDeleted++;
215
+
216
+ } catch (error) {
217
+ console.error(`[FileWatcher] Error deleting ${filePath}:`, error.message);
218
+ this.stats.errors++;
219
+ }
40
220
  }
41
221
 
42
- async flushQueue() {
43
- const updates = Array.from(this.pendingUpdates.values());
44
- this.pendingUpdates.clear();
45
- await Promise.all(updates.map(u => this.processUpdate(u)));
222
+ async indexMarkdownFile(filePath, content, contentHash, stats) {
223
+ console.log(`[FileWatcher] Indexing Markdown: ${filePath}`);
224
+
225
+ // Delete old chunks if updating
226
+ const relativePath = path.relative(this.projectPath, filePath);
227
+ this.memory.memoryDb?.deleteChunksByFile?.(relativePath);
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);
242
+
243
+ if (cached) {
244
+ embedding = cached.embedding;
245
+ } else {
246
+ // Generate embedding
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
+ }
259
+
260
+ // Insert chunk
261
+ this.memory.memoryDb?.insertChunk?.({
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
+ });
270
+
271
+ // Insert embedding
272
+ if (embedding) {
273
+ this.memory.memoryDb?.insertEmbedding?.({
274
+ chunk_id: chunk.id,
275
+ embedding: Buffer.from(new Float32Array(embedding).buffer),
276
+ model: 'simple-embedder',
277
+ dimensions: embedding.length
278
+ });
279
+ }
280
+ }
281
+
282
+ console.log(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
46
283
  }
47
284
 
48
- async processUpdate(update) {
49
- console.log(`[Watcher] ${update.type}: ${update.path}`);
285
+ async indexCodeFile(filePath, content, contentHash, stats) {
286
+ console.log(`[FileWatcher] Indexing code: ${filePath}`);
287
+
288
+ // Use the existing indexer
289
+ if (this.memory.indexer) {
290
+ try {
291
+ const indexed = await this.memory.indexer.indexFile(filePath, content);
292
+ await this.memory.storeIndexed(indexed);
293
+ console.log(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks from ${filePath}`);
294
+ } catch (error) {
295
+ console.error(`[FileWatcher] Failed to index ${filePath}:`, error.message);
296
+ }
297
+ }
50
298
  }
51
299
 
52
- async handleDelete(filePath) {
53
- console.log(`[Watcher] Deleted: ${filePath}`);
300
+ isMarkdownFile(filePath) {
301
+ const ext = path.extname(filePath).toLowerCase();
302
+ return ext === '.md' || ext === '.markdown';
303
+ }
304
+
305
+ getStats() {
306
+ return {
307
+ ...this.stats,
308
+ pendingUpdates: this.pendingUpdates.size,
309
+ isProcessing: this.isProcessing
310
+ };
54
311
  }
55
312
 
56
313
  async stop() {
57
- if (this.watcher) await this.watcher.close();
314
+ if (this.watcher) {
315
+ // Process any remaining updates
316
+ if (this.pendingUpdates.size > 0) {
317
+ await this.processQueue();
318
+ }
319
+
320
+ await this.watcher.close();
321
+ this.watcher = null;
322
+ console.log(`[FileWatcher] Stopped`);
323
+ }
58
324
  }
59
325
  }
60
326