@mrxkun/mcfast-mcp 4.0.14 → 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
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase Database
|
|
3
|
+
* Lưu trữ index cho codebase (files, facts, chunks)
|
|
4
|
+
* Separate from MemoryDatabase to keep code and notes separate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
|
|
11
|
+
export class CodebaseDatabase {
|
|
12
|
+
constructor(dbPath = null) {
|
|
13
|
+
this.dbPath = dbPath;
|
|
14
|
+
this.db = null;
|
|
15
|
+
this.isInitialized = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (this.isInitialized) return;
|
|
20
|
+
|
|
21
|
+
await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
22
|
+
|
|
23
|
+
this.db = new Database(this.dbPath);
|
|
24
|
+
this.db.pragma('journal_mode = WAL');
|
|
25
|
+
|
|
26
|
+
this.createTables();
|
|
27
|
+
this.isInitialized = true;
|
|
28
|
+
|
|
29
|
+
console.log(`[CodebaseDatabase] Initialized at: ${this.dbPath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
createTables() {
|
|
33
|
+
// Files table
|
|
34
|
+
this.db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
path TEXT UNIQUE NOT NULL,
|
|
38
|
+
content_hash TEXT,
|
|
39
|
+
last_modified INTEGER,
|
|
40
|
+
language TEXT,
|
|
41
|
+
line_count INTEGER,
|
|
42
|
+
indexed_at INTEGER
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_files_hash ON files(content_hash);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_files_language ON files(language);
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
// Facts table (functions, classes, etc.)
|
|
51
|
+
this.db.exec(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
file_id TEXT NOT NULL,
|
|
55
|
+
type TEXT NOT NULL,
|
|
56
|
+
name TEXT NOT NULL,
|
|
57
|
+
line_start INTEGER,
|
|
58
|
+
line_end INTEGER,
|
|
59
|
+
signature TEXT,
|
|
60
|
+
exported BOOLEAN,
|
|
61
|
+
documentation TEXT,
|
|
62
|
+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_facts_file ON facts(file_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_facts_name ON facts(name);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_facts_type ON facts(type);
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
// Chunks table (code segments)
|
|
71
|
+
this.db.exec(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
file_id TEXT NOT NULL,
|
|
75
|
+
content TEXT NOT NULL,
|
|
76
|
+
content_hash TEXT,
|
|
77
|
+
start_line INTEGER,
|
|
78
|
+
end_line INTEGER,
|
|
79
|
+
token_count INTEGER,
|
|
80
|
+
chunk_type TEXT DEFAULT 'code',
|
|
81
|
+
created_at INTEGER,
|
|
82
|
+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(content_hash);
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
// FTS5 for code search
|
|
90
|
+
this.db.exec(`
|
|
91
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
92
|
+
content,
|
|
93
|
+
content_rowid=id,
|
|
94
|
+
tokenize='porter'
|
|
95
|
+
);
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// Triggers to sync FTS5
|
|
99
|
+
this.db.exec(`
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
|
|
101
|
+
INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);
|
|
102
|
+
END;
|
|
103
|
+
|
|
104
|
+
CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
|
|
105
|
+
INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
106
|
+
END;
|
|
107
|
+
|
|
108
|
+
CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
|
|
109
|
+
INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
110
|
+
INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);
|
|
111
|
+
END;
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
// Embeddings for code chunks
|
|
115
|
+
this.db.exec(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
117
|
+
chunk_id TEXT PRIMARY KEY,
|
|
118
|
+
embedding BLOB NOT NULL,
|
|
119
|
+
model TEXT,
|
|
120
|
+
dimensions INTEGER,
|
|
121
|
+
created_at INTEGER,
|
|
122
|
+
FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_embeddings_model ON embeddings(model);
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
// Edit history
|
|
129
|
+
this.db.exec(`
|
|
130
|
+
CREATE TABLE IF NOT EXISTS edit_history (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
timestamp INTEGER,
|
|
133
|
+
instruction TEXT,
|
|
134
|
+
files TEXT,
|
|
135
|
+
strategy TEXT,
|
|
136
|
+
success BOOLEAN,
|
|
137
|
+
diff_size INTEGER,
|
|
138
|
+
latency_ms INTEGER
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_edit_timestamp ON edit_history(timestamp);
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
// Indexing progress (for initial scan)
|
|
145
|
+
this.db.exec(`
|
|
146
|
+
CREATE TABLE IF NOT EXISTS indexing_progress (
|
|
147
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
148
|
+
total_files INTEGER DEFAULT 0,
|
|
149
|
+
indexed_files INTEGER DEFAULT 0,
|
|
150
|
+
failed_files INTEGER DEFAULT 0,
|
|
151
|
+
started_at INTEGER,
|
|
152
|
+
completed_at INTEGER,
|
|
153
|
+
is_complete BOOLEAN DEFAULT 0
|
|
154
|
+
);
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ========== File Operations ==========
|
|
159
|
+
|
|
160
|
+
upsertFile(file) {
|
|
161
|
+
const stmt = this.db.prepare(`
|
|
162
|
+
INSERT OR REPLACE INTO files
|
|
163
|
+
(id, path, content_hash, last_modified, language, line_count, indexed_at)
|
|
164
|
+
VALUES ($id, $path, $content_hash, $last_modified, $language, $line_count, $indexed_at)
|
|
165
|
+
`);
|
|
166
|
+
return stmt.run({
|
|
167
|
+
...file,
|
|
168
|
+
indexed_at: Date.now()
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getFileByPath(filePath) {
|
|
173
|
+
return this.db.prepare('SELECT * FROM files WHERE path = ?').get(filePath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getFileById(fileId) {
|
|
177
|
+
return this.db.prepare('SELECT * FROM files WHERE id = ?').get(fileId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
deleteFile(fileId) {
|
|
181
|
+
return this.db.prepare('DELETE FROM files WHERE id = ?').run(fileId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getAllFiles() {
|
|
185
|
+
return this.db.prepare('SELECT * FROM files ORDER BY path').all();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getFilesByLanguage(language) {
|
|
189
|
+
return this.db.prepare('SELECT * FROM files WHERE language = ?').all(language);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
isFileIndexed(filePath, contentHash) {
|
|
193
|
+
const file = this.getFileByPath(filePath);
|
|
194
|
+
return file && file.content_hash === contentHash;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ========== Fact Operations ==========
|
|
198
|
+
|
|
199
|
+
insertFact(fact) {
|
|
200
|
+
const stmt = this.db.prepare(`
|
|
201
|
+
INSERT OR REPLACE INTO facts
|
|
202
|
+
(id, file_id, type, name, line_start, line_end, signature, exported, documentation)
|
|
203
|
+
VALUES ($id, $file_id, $type, $name, $line_start, $line_end, $signature, $exported, $documentation)
|
|
204
|
+
`);
|
|
205
|
+
return stmt.run(fact);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
deleteFactsByFile(fileId) {
|
|
209
|
+
return this.db.prepare('DELETE FROM facts WHERE file_id = ?').run(fileId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
searchFacts(query, limit = 20) {
|
|
213
|
+
return this.db.prepare(`
|
|
214
|
+
SELECT f.*, files.path as file_path
|
|
215
|
+
FROM facts f
|
|
216
|
+
JOIN files ON f.file_id = files.id
|
|
217
|
+
WHERE f.name LIKE ?
|
|
218
|
+
LIMIT ?
|
|
219
|
+
`).all(`%${query}%`, limit);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getFactsByFile(fileId) {
|
|
223
|
+
return this.db.prepare('SELECT * FROM facts WHERE file_id = ?').all(fileId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ========== Chunk Operations ==========
|
|
227
|
+
|
|
228
|
+
insertChunk(chunk) {
|
|
229
|
+
const stmt = this.db.prepare(`
|
|
230
|
+
INSERT OR REPLACE INTO chunks
|
|
231
|
+
(id, file_id, content, content_hash, start_line, end_line, token_count, chunk_type, created_at)
|
|
232
|
+
VALUES ($id, $file_id, $content, $content_hash, $start_line, $end_line, $token_count, $chunk_type, $created_at)
|
|
233
|
+
`);
|
|
234
|
+
return stmt.run({
|
|
235
|
+
...chunk,
|
|
236
|
+
created_at: chunk.created_at || Date.now()
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
deleteChunksByFile(fileId) {
|
|
241
|
+
return this.db.prepare('DELETE FROM chunks WHERE file_id = ?').run(fileId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
getChunksByFile(fileId) {
|
|
245
|
+
return this.db.prepare(`
|
|
246
|
+
SELECT c.*, f.path as file_path
|
|
247
|
+
FROM chunks c
|
|
248
|
+
JOIN files f ON c.file_id = f.id
|
|
249
|
+
WHERE c.file_id = ?
|
|
250
|
+
ORDER BY c.start_line
|
|
251
|
+
`).all(fileId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getAllChunksWithContent() {
|
|
255
|
+
return this.db.prepare(`
|
|
256
|
+
SELECT c.*, f.path as file_path, e.embedding
|
|
257
|
+
FROM chunks c
|
|
258
|
+
JOIN files f ON c.file_id = f.id
|
|
259
|
+
LEFT JOIN embeddings e ON c.id = e.chunk_id
|
|
260
|
+
`).all();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getRecentChunks(limit = 100) {
|
|
264
|
+
return this.db.prepare(`
|
|
265
|
+
SELECT c.*, f.path as file_path
|
|
266
|
+
FROM chunks c
|
|
267
|
+
JOIN files f ON c.file_id = f.id
|
|
268
|
+
ORDER BY c.created_at DESC
|
|
269
|
+
LIMIT ?
|
|
270
|
+
`).all(limit);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ========== Embedding Operations ==========
|
|
274
|
+
|
|
275
|
+
insertEmbedding(embedding) {
|
|
276
|
+
const stmt = this.db.prepare(`
|
|
277
|
+
INSERT OR REPLACE INTO embeddings (chunk_id, embedding, model, dimensions, created_at)
|
|
278
|
+
VALUES ($chunk_id, $embedding, $model, $dimensions, $created_at)
|
|
279
|
+
`);
|
|
280
|
+
return stmt.run({
|
|
281
|
+
...embedding,
|
|
282
|
+
created_at: embedding.created_at || Date.now()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getEmbedding(chunkId) {
|
|
287
|
+
return this.db.prepare('SELECT * FROM embeddings WHERE chunk_id = ?').get(chunkId);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
getAllEmbeddings() {
|
|
291
|
+
return this.db.prepare(`
|
|
292
|
+
SELECT e.*, c.content, c.file_id, c.start_line, c.end_line, f.path as file_path
|
|
293
|
+
FROM embeddings e
|
|
294
|
+
JOIN chunks c ON e.chunk_id = c.id
|
|
295
|
+
JOIN files f ON c.file_id = f.id
|
|
296
|
+
`).all();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ========== Search Operations ==========
|
|
300
|
+
|
|
301
|
+
searchFTS(query, limit = 20) {
|
|
302
|
+
const startTime = performance.now();
|
|
303
|
+
|
|
304
|
+
const stmt = this.db.prepare(`
|
|
305
|
+
SELECT
|
|
306
|
+
c.*,
|
|
307
|
+
f.path as file_path,
|
|
308
|
+
rank as bm25_score
|
|
309
|
+
FROM chunks_fts fts
|
|
310
|
+
JOIN chunks c ON fts.rowid = c.id
|
|
311
|
+
JOIN files f ON c.file_id = f.id
|
|
312
|
+
WHERE chunks_fts MATCH ?
|
|
313
|
+
ORDER BY rank
|
|
314
|
+
LIMIT ?
|
|
315
|
+
`);
|
|
316
|
+
|
|
317
|
+
const results = stmt.all(query, limit);
|
|
318
|
+
const duration = performance.now() - startTime;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
results: results.map(r => ({
|
|
322
|
+
chunk_id: r.id,
|
|
323
|
+
file_path: r.file_path,
|
|
324
|
+
start_line: r.start_line,
|
|
325
|
+
end_line: r.end_line,
|
|
326
|
+
content: r.content,
|
|
327
|
+
score: 1 / (1 + Math.max(0, r.bm25_score))
|
|
328
|
+
})),
|
|
329
|
+
metadata: {
|
|
330
|
+
method: 'fts5',
|
|
331
|
+
duration: duration.toFixed(2) + 'ms'
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ========== Edit History ==========
|
|
337
|
+
|
|
338
|
+
recordEdit(edit) {
|
|
339
|
+
const stmt = this.db.prepare(`
|
|
340
|
+
INSERT INTO edit_history (id, timestamp, instruction, files, strategy, success, diff_size, latency_ms)
|
|
341
|
+
VALUES ($id, $timestamp, $instruction, $files, $strategy, $success, $diff_size, $latency_ms)
|
|
342
|
+
`);
|
|
343
|
+
return stmt.run({
|
|
344
|
+
...edit,
|
|
345
|
+
timestamp: edit.timestamp || Date.now()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getRecentEdits(limit = 100) {
|
|
350
|
+
return this.db.prepare('SELECT * FROM edit_history ORDER BY timestamp DESC LIMIT ?').all(limit);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ========== Indexing Progress ==========
|
|
354
|
+
|
|
355
|
+
startIndexing(totalFiles) {
|
|
356
|
+
const stmt = this.db.prepare(`
|
|
357
|
+
INSERT OR REPLACE INTO indexing_progress
|
|
358
|
+
(id, total_files, indexed_files, failed_files, started_at, completed_at, is_complete)
|
|
359
|
+
VALUES (1, ?, 0, 0, ?, NULL, 0)
|
|
360
|
+
`);
|
|
361
|
+
return stmt.run(totalFiles, Date.now());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
incrementIndexed(failed = false) {
|
|
365
|
+
const column = failed ? 'failed_files' : 'indexed_files';
|
|
366
|
+
return this.db.prepare(`
|
|
367
|
+
UPDATE indexing_progress
|
|
368
|
+
SET ${column} = ${column} + 1
|
|
369
|
+
WHERE id = 1
|
|
370
|
+
`).run();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
completeIndexing() {
|
|
374
|
+
return this.db.prepare(`
|
|
375
|
+
UPDATE indexing_progress
|
|
376
|
+
SET completed_at = ?, is_complete = 1
|
|
377
|
+
WHERE id = 1
|
|
378
|
+
`).run(Date.now());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getIndexingProgress() {
|
|
382
|
+
return this.db.prepare('SELECT * FROM indexing_progress WHERE id = 1').get();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ========== Stats ==========
|
|
386
|
+
|
|
387
|
+
getStats() {
|
|
388
|
+
const files = this.db.prepare('SELECT COUNT(*) as count FROM files').get();
|
|
389
|
+
const facts = this.db.prepare('SELECT COUNT(*) as count FROM facts').get();
|
|
390
|
+
const chunks = this.db.prepare('SELECT COUNT(*) as count FROM chunks').get();
|
|
391
|
+
const embeddings = this.db.prepare('SELECT COUNT(*) as count FROM embeddings').get();
|
|
392
|
+
const edits = this.db.prepare('SELECT COUNT(*) as count FROM edit_history').get();
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
files: files.count,
|
|
396
|
+
facts: facts.count,
|
|
397
|
+
chunks: chunks.count,
|
|
398
|
+
embeddings: embeddings.count,
|
|
399
|
+
edits: edits.count,
|
|
400
|
+
dbPath: this.dbPath
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ========== Maintenance ==========
|
|
405
|
+
|
|
406
|
+
vacuum() {
|
|
407
|
+
this.db.exec('VACUUM');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
close() {
|
|
411
|
+
if (this.db) {
|
|
412
|
+
this.db.close();
|
|
413
|
+
this.isInitialized = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export default CodebaseDatabase;
|