@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.
- package/package.json +2 -2
- package/src/memory/bootstrap/agents-md.js +173 -0
- package/src/memory/index.js +26 -13
- package/src/memory/layers/curated-memory.js +324 -0
- package/src/memory/layers/daily-logs.js +236 -0
- package/src/memory/memory-engine.js +472 -452
- package/src/memory/stores/codebase-database.js +418 -0
- package/src/memory/stores/memory-database.js +425 -0
- package/src/memory/utils/markdown-chunker.js +242 -0
- package/src/memory/watchers/file-watcher.js +286 -20
- package/src/tools/memory_get.js +139 -100
- package/src/tools/memory_search.js +118 -86
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
console.log(`[
|
|
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
|
-
|
|
53
|
-
|
|
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)
|
|
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
|
|