@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.
@@ -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;