@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
|
@@ -26,6 +26,9 @@ import { CuratedMemory } from './layers/curated-memory.js';
|
|
|
26
26
|
// Bootstrap
|
|
27
27
|
import { AgentsMdBootstrap } from './bootstrap/agents-md.js';
|
|
28
28
|
|
|
29
|
+
// Project Analysis (AI-powered)
|
|
30
|
+
import { execute as projectAnalyzeExecute } from '../tools/project_analyze.js';
|
|
31
|
+
|
|
29
32
|
// Embedders
|
|
30
33
|
import { UltraEnhancedEmbedder } from './utils/ultra-embedder.js';
|
|
31
34
|
import { SmartRouter } from './utils/smart-router.js';
|
|
@@ -38,6 +41,7 @@ import { PatternDetector, SuggestionEngine, StrategySelector } from '../intellig
|
|
|
38
41
|
export class MemoryEngine {
|
|
39
42
|
static instance = null;
|
|
40
43
|
static instancePromise = null;
|
|
44
|
+
static instancePid = null; // Track which process owns the instance
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Get singleton instance of MemoryEngine
|
|
@@ -45,8 +49,19 @@ export class MemoryEngine {
|
|
|
45
49
|
* @returns {MemoryEngine} Singleton instance
|
|
46
50
|
*/
|
|
47
51
|
static getInstance(options = {}) {
|
|
52
|
+
const currentPid = process.pid;
|
|
53
|
+
|
|
54
|
+
// Check if instance belongs to current process
|
|
55
|
+
if (MemoryEngine.instancePid !== currentPid) {
|
|
56
|
+
// Different process - reset instance
|
|
57
|
+
MemoryEngine.instance = null;
|
|
58
|
+
MemoryEngine.instancePromise = null;
|
|
59
|
+
MemoryEngine.instancePid = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
if (!MemoryEngine.instance) {
|
|
49
63
|
MemoryEngine.instance = new MemoryEngine(options);
|
|
64
|
+
MemoryEngine.instancePid = currentPid;
|
|
50
65
|
}
|
|
51
66
|
return MemoryEngine.instance;
|
|
52
67
|
}
|
|
@@ -58,6 +73,16 @@ export class MemoryEngine {
|
|
|
58
73
|
* @returns {Promise<MemoryEngine>} Initialized singleton instance
|
|
59
74
|
*/
|
|
60
75
|
static async getOrCreate(projectPath, options = {}) {
|
|
76
|
+
const currentPid = process.pid;
|
|
77
|
+
|
|
78
|
+
// Check if instance belongs to current process
|
|
79
|
+
if (MemoryEngine.instancePid !== currentPid) {
|
|
80
|
+
// Different process - reset instance
|
|
81
|
+
MemoryEngine.instance = null;
|
|
82
|
+
MemoryEngine.instancePromise = null;
|
|
83
|
+
MemoryEngine.instancePid = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
if (MemoryEngine.instance && MemoryEngine.instance.isInitialized) {
|
|
62
87
|
return MemoryEngine.instance;
|
|
63
88
|
}
|
|
@@ -68,6 +93,7 @@ export class MemoryEngine {
|
|
|
68
93
|
|
|
69
94
|
const engine = new MemoryEngine(options);
|
|
70
95
|
MemoryEngine.instance = engine;
|
|
96
|
+
MemoryEngine.instancePid = currentPid;
|
|
71
97
|
MemoryEngine.instancePromise = engine.initialize(projectPath).then(() => engine);
|
|
72
98
|
|
|
73
99
|
return MemoryEngine.instancePromise;
|
|
@@ -131,6 +157,9 @@ export class MemoryEngine {
|
|
|
131
157
|
this.suggestionEngine = null;
|
|
132
158
|
this.strategySelector = null;
|
|
133
159
|
|
|
160
|
+
// Auto-analyze project on first use
|
|
161
|
+
this.autoAnalyze = options.autoAnalyze !== false;
|
|
162
|
+
|
|
134
163
|
// Search configuration
|
|
135
164
|
this.searchConfig = {
|
|
136
165
|
hybrid: {
|
|
@@ -196,19 +225,32 @@ export class MemoryEngine {
|
|
|
196
225
|
});
|
|
197
226
|
}
|
|
198
227
|
|
|
199
|
-
// Initialize file watcher
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
// Initialize file watcher (configurable via MCFAST_FILE_WATCHER env)
|
|
229
|
+
const ENABLE_FILE_WATCHER = process.env.MCFAST_FILE_WATCHER !== 'false';
|
|
230
|
+
|
|
231
|
+
if (ENABLE_FILE_WATCHER) {
|
|
232
|
+
this.watcher = new FileWatcher(projectPath, this, {
|
|
233
|
+
debounceMs: 1500,
|
|
234
|
+
ignored: [
|
|
235
|
+
'**/node_modules/**',
|
|
236
|
+
'**/.git/**',
|
|
237
|
+
'**/dist/**',
|
|
238
|
+
'**/build/**',
|
|
239
|
+
'**/.mcfast/**',
|
|
240
|
+
'**/*.log'
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await this.watcher.start();
|
|
246
|
+
} catch (watcherError) {
|
|
247
|
+
console.error('[MemoryEngine] ⚠️ File watcher failed to start (continuing without file watching):', watcherError.message);
|
|
248
|
+
this.watcher = null;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
console.error('[MemoryEngine] File watcher disabled (MCFAST_FILE_WATCHER=false)');
|
|
252
|
+
this.watcher = null;
|
|
253
|
+
}
|
|
212
254
|
|
|
213
255
|
// Perform initial scan
|
|
214
256
|
await this.performInitialScan();
|
|
@@ -223,6 +265,9 @@ export class MemoryEngine {
|
|
|
223
265
|
console.error(`[MemoryEngine] Smart Routing: ${this.smartRoutingEnabled ? 'ENABLED' : 'DISABLED'}`);
|
|
224
266
|
console.error(`[MemoryEngine] Intelligence: ${this.intelligenceEnabled ? 'ENABLED' : 'DISABLED'}`);
|
|
225
267
|
|
|
268
|
+
// Auto-analyze project if MCFAST_TOKEN is set
|
|
269
|
+
await this.autoAnalyzeProject();
|
|
270
|
+
|
|
226
271
|
// Log initialization to daily logs
|
|
227
272
|
await this.dailyLogs.log('Memory Engine Initialized', `Project: ${projectPath}`, {
|
|
228
273
|
stats: this.getStats()
|
|
@@ -264,6 +309,47 @@ export class MemoryEngine {
|
|
|
264
309
|
}
|
|
265
310
|
}
|
|
266
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Auto-analyze project using AI if MCFAST_TOKEN is set
|
|
314
|
+
* Runs in background to not block initialization
|
|
315
|
+
*/
|
|
316
|
+
async autoAnalyzeProject() {
|
|
317
|
+
if (!this.autoAnalyze) return;
|
|
318
|
+
|
|
319
|
+
const apiKey = process.env.MCFAST_TOKEN;
|
|
320
|
+
if (!apiKey) {
|
|
321
|
+
console.error('[MemoryEngine] Skipping auto-analysis (no MCFAST_TOKEN)');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check if already analyzed (MEMORY.md has Project Context section)
|
|
326
|
+
try {
|
|
327
|
+
const memoryContent = await this.curatedMemory.read();
|
|
328
|
+
if (memoryContent && memoryContent.includes('## Project Context') &&
|
|
329
|
+
!memoryContent.includes('<!-- Add project context here -->')) {
|
|
330
|
+
console.error('[MemoryEngine] Project already analyzed, skipping auto-analysis');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
// MEMORY.md doesn't exist yet, proceed with analysis
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Run analysis in background
|
|
338
|
+
console.error('[MemoryEngine] 🔄 Starting auto-analysis in background...');
|
|
339
|
+
|
|
340
|
+
projectAnalyzeExecute({ force: false, updateMemory: true })
|
|
341
|
+
.then(result => {
|
|
342
|
+
if (result.metadata?.memoryUpdated) {
|
|
343
|
+
console.error('[MemoryEngine] ✅ Auto-analysis complete, MEMORY.md updated');
|
|
344
|
+
} else if (result.isError) {
|
|
345
|
+
console.error('[MemoryEngine] ⚠️ Auto-analysis failed:', result.content?.[0]?.text?.substring(0, 100));
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
.catch(err => {
|
|
349
|
+
console.error('[MemoryEngine] ⚠️ Auto-analysis error:', err.message);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
267
353
|
async performInitialScan() {
|
|
268
354
|
if (this.isScanning) return;
|
|
269
355
|
this.isScanning = true;
|
|
@@ -470,24 +556,44 @@ export class MemoryEngine {
|
|
|
470
556
|
|
|
471
557
|
// ========== Storage Operations ==========
|
|
472
558
|
|
|
473
|
-
async storeIndexed(indexed) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
559
|
+
async storeIndexed(indexed, maxRetries = 3) {
|
|
560
|
+
// Retry logic with exponential backoff
|
|
561
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
562
|
+
try {
|
|
563
|
+
this.codebaseDb?.upsertFile?.(indexed.file);
|
|
564
|
+
this.codebaseDb?.deleteFactsByFile?.(indexed.file.id);
|
|
565
|
+
this.codebaseDb?.deleteChunksByFile?.(indexed.file.id);
|
|
477
566
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
567
|
+
for (const fact of indexed.facts) {
|
|
568
|
+
this.codebaseDb?.insertFact?.(fact);
|
|
569
|
+
}
|
|
481
570
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
571
|
+
for (const chunk of indexed.chunks) {
|
|
572
|
+
this.codebaseDb?.insertChunk?.(chunk);
|
|
573
|
+
}
|
|
485
574
|
|
|
486
|
-
|
|
487
|
-
|
|
575
|
+
for (const embedding of indexed.embeddings) {
|
|
576
|
+
this.codebaseDb?.insertEmbedding?.(embedding);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return true; // Success
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (attempt === maxRetries) {
|
|
582
|
+
console.error(`[MemoryEngine] Failed to store indexed data after ${maxRetries} attempts:`, error.message);
|
|
583
|
+
this.codebaseDb?.recordFailedIndex?.(indexed.file.path, error.message);
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
// Exponential backoff: 100ms, 200ms, 400ms
|
|
587
|
+
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
|
|
588
|
+
}
|
|
488
589
|
}
|
|
489
590
|
}
|
|
490
591
|
|
|
592
|
+
// Record failed indexing for tracking
|
|
593
|
+
recordFailedIndex(filePath, errorMessage) {
|
|
594
|
+
console.error(`[MemoryEngine] Indexing failed for ${filePath}: ${errorMessage}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
491
597
|
// ========== Search Operations ==========
|
|
492
598
|
|
|
493
599
|
async searchFacts(query, limit = 20) {
|
|
@@ -573,6 +679,80 @@ export class MemoryEngine {
|
|
|
573
679
|
}
|
|
574
680
|
|
|
575
681
|
async searchCodebase(query, limit = 20) {
|
|
682
|
+
const startTime = performance.now();
|
|
683
|
+
|
|
684
|
+
// FIX: Get embeddings from both codebaseDb AND memoryDb
|
|
685
|
+
const [codebaseEmbeddings, memoryEmbeddings] = await Promise.all([
|
|
686
|
+
this.codebaseDb?.getAllEmbeddings?.() || [],
|
|
687
|
+
this.memoryDb?.getAllEmbeddings?.() || []
|
|
688
|
+
]);
|
|
689
|
+
|
|
690
|
+
// Generate query embedding
|
|
691
|
+
const queryResult = this.embedder.embedCode(query);
|
|
692
|
+
const queryEmbedding = queryResult.vector;
|
|
693
|
+
|
|
694
|
+
// Score and tag results from each source
|
|
695
|
+
const scoreFn = (item) => {
|
|
696
|
+
const buf = item.embedding?.buffer || item.embedding;
|
|
697
|
+
const arr = buf ? new Uint8Array(buf) : new Uint8Array(0);
|
|
698
|
+
return this.embedder.cosineSimilarity(queryEmbedding, Array.from(arr));
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const codebaseResults = (codebaseEmbeddings || []).map(item => ({
|
|
702
|
+
...item, similarity: scoreFn(item), source: 'codebase', type: 'code'
|
|
703
|
+
}));
|
|
704
|
+
|
|
705
|
+
const memoryResults = (memoryEmbeddings || []).map(item => ({
|
|
706
|
+
...item, similarity: scoreFn(item), source: 'memory', type: 'memory'
|
|
707
|
+
}));
|
|
708
|
+
|
|
709
|
+
// Combine results
|
|
710
|
+
let combined = [...codebaseResults, ...memoryResults];
|
|
711
|
+
|
|
712
|
+
// Also get FTS results from both databases
|
|
713
|
+
const [codebaseFts, memoryFts] = await Promise.all([
|
|
714
|
+
this.codebaseDb?.searchFTS?.(query, limit * 2) || { results: [] },
|
|
715
|
+
this.memoryDb?.searchFTS?.(query, limit * 2) || { results: [] }
|
|
716
|
+
]);
|
|
717
|
+
|
|
718
|
+
// Add FTS results
|
|
719
|
+
for (const r of codebaseFts.results || []) {
|
|
720
|
+
combined.push({ ...r, source: 'fts', type: 'code', score: r.score });
|
|
721
|
+
}
|
|
722
|
+
for (const r of memoryFts.results || []) {
|
|
723
|
+
combined.push({ ...r, source: 'fts', type: 'memory', score: r.score });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Deduplicate and sort
|
|
727
|
+
const seen = new Map();
|
|
728
|
+
for (const r of combined) {
|
|
729
|
+
const key = r.chunk_id || r.id;
|
|
730
|
+
if (!seen.has(key) || (r.similarity || r.score || 0) > (seen.get(key).similarity || seen.get(key).score || 0)) {
|
|
731
|
+
seen.set(key, r);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const finalResults = Array.from(seen.values())
|
|
736
|
+
.sort((a, b) => (b.similarity || b.score || 0) - (a.similarity || a.score || 0))
|
|
737
|
+
.slice(0, limit);
|
|
738
|
+
|
|
739
|
+
const duration = performance.now() - startTime;
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
results: finalResults,
|
|
743
|
+
metadata: {
|
|
744
|
+
vectorCandidates: codebaseResults.length + memoryResults.length,
|
|
745
|
+
ftsCandidates: (codebaseFts.results?.length || 0) + (memoryFts.results?.length || 0),
|
|
746
|
+
totalCandidates: combined.length,
|
|
747
|
+
codebaseResults: codebaseResults.length,
|
|
748
|
+
memoryResults: memoryResults.length,
|
|
749
|
+
duration: duration.toFixed(2) + 'ms'
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Legacy search - kept for backward compatibility
|
|
755
|
+
async _searchCodebaseLegacy(query, limit = 20) {
|
|
576
756
|
// Search codebase using existing methods
|
|
577
757
|
const vectorResults = await this.searchVector(query, limit * 2);
|
|
578
758
|
const ftsResults = this.codebaseDb?.searchFTS?.(query, limit * 2);
|
|
@@ -781,6 +961,33 @@ export class MemoryEngine {
|
|
|
781
961
|
this.isInitialized = false;
|
|
782
962
|
console.error('[MemoryEngine] Shutdown complete');
|
|
783
963
|
}
|
|
964
|
+
|
|
965
|
+
async cleanup() {
|
|
966
|
+
// Alias for shutdown for consistency
|
|
967
|
+
return this.shutdown();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Auto-register cleanup handlers for graceful shutdown
|
|
972
|
+
if (typeof process !== 'undefined') {
|
|
973
|
+
const cleanupHandler = async (signal) => {
|
|
974
|
+
console.error(`[MemoryEngine] Received ${signal}, performing cleanup...`);
|
|
975
|
+
if (MemoryEngine.instance) {
|
|
976
|
+
try {
|
|
977
|
+
await MemoryEngine.instance.shutdown();
|
|
978
|
+
} catch (error) {
|
|
979
|
+
console.error(`[MemoryEngine] Error during shutdown: ${error.message}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// Register handlers only once
|
|
985
|
+
if (!process._memoryEngineCleanupRegistered) {
|
|
986
|
+
process.on('beforeExit', () => cleanupHandler('beforeExit'));
|
|
987
|
+
process.on('SIGINT', () => cleanupHandler('SIGINT'));
|
|
988
|
+
process.on('SIGTERM', () => cleanupHandler('SIGTERM'));
|
|
989
|
+
process._memoryEngineCleanupRegistered = true;
|
|
990
|
+
}
|
|
784
991
|
}
|
|
785
992
|
|
|
786
993
|
export default MemoryEngine;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Database Class
|
|
3
|
+
* Common methods for MemoryDatabase and CodebaseDatabase
|
|
4
|
+
* Reduces code duplication between the two database classes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
|
|
11
|
+
export class BaseDatabase {
|
|
12
|
+
constructor(dbPath = null, options = {}) {
|
|
13
|
+
this.dbPath = dbPath;
|
|
14
|
+
this.db = null;
|
|
15
|
+
this.isInitialized = false;
|
|
16
|
+
this.options = options;
|
|
17
|
+
|
|
18
|
+
// Logger (can be replaced with proper logger)
|
|
19
|
+
this.logger = options.logger || console;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize() {
|
|
23
|
+
if (this.isInitialized) return;
|
|
24
|
+
|
|
25
|
+
await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
26
|
+
|
|
27
|
+
this.db = new Database(this.dbPath);
|
|
28
|
+
this.db.pragma('journal_mode = WAL');
|
|
29
|
+
|
|
30
|
+
this.createTables();
|
|
31
|
+
this.isInitialized = true;
|
|
32
|
+
|
|
33
|
+
this._log(`Initialized at: ${this.dbPath}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Override in subclass to create specific tables
|
|
38
|
+
*/
|
|
39
|
+
createTables() {
|
|
40
|
+
throw new Error('createTables must be implemented in subclass');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Common chunk operations
|
|
45
|
+
*/
|
|
46
|
+
insertChunk(chunk) {
|
|
47
|
+
throw new Error('insertChunk must be implemented in subclass');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deleteChunksByFile(filePath) {
|
|
51
|
+
throw new Error('deleteChunksByFile must be implemented in subclass');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getChunksByFile(filePath, limit = 100) {
|
|
55
|
+
throw new Error('getChunksByFile must be implemented in subclass');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getRecentChunks(limit = 100) {
|
|
59
|
+
throw new Error('getRecentChunks must be implemented in subclass');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Common embedding operations
|
|
64
|
+
*/
|
|
65
|
+
insertEmbedding(embedding) {
|
|
66
|
+
throw new Error('insertEmbedding must be implemented in subclass');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getEmbedding(chunkId) {
|
|
70
|
+
throw new Error('getEmbedding must be implemented in subclass');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getAllEmbeddings() {
|
|
74
|
+
throw new Error('getAllEmbeddings must be implemented in subclass');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Common search operations (FTS5)
|
|
79
|
+
*/
|
|
80
|
+
searchFTS(query, limit = 20) {
|
|
81
|
+
const startTime = performance.now();
|
|
82
|
+
|
|
83
|
+
const stmt = this.db.prepare(`
|
|
84
|
+
SELECT
|
|
85
|
+
c.*,
|
|
86
|
+
rank as bm25_score
|
|
87
|
+
FROM chunks_fts fts
|
|
88
|
+
JOIN chunks c ON fts.rowid = c.id
|
|
89
|
+
WHERE chunks_fts MATCH ?
|
|
90
|
+
ORDER BY rank
|
|
91
|
+
LIMIT ?
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
const results = stmt.all(query, limit);
|
|
95
|
+
const duration = performance.now() - startTime;
|
|
96
|
+
|
|
97
|
+
// Log search
|
|
98
|
+
this.logSearch(query, 'fts', results.length, duration);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
results: results.map(r => ({
|
|
102
|
+
...r,
|
|
103
|
+
score: 1 / (1 + Math.max(0, r.bm25_score))
|
|
104
|
+
})),
|
|
105
|
+
metadata: {
|
|
106
|
+
method: 'fts5',
|
|
107
|
+
duration: duration.toFixed(2) + 'ms',
|
|
108
|
+
candidates: results.length
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Common search history logging
|
|
115
|
+
*/
|
|
116
|
+
logSearch(query, method, resultsCount, durationMs) {
|
|
117
|
+
try {
|
|
118
|
+
const stmt = this.db.prepare(`
|
|
119
|
+
INSERT INTO search_history (query, method, results_count, duration_ms, timestamp)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?)
|
|
121
|
+
`);
|
|
122
|
+
stmt.run(query, method, resultsCount, Math.round(durationMs), Date.now());
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Silent fail - don't break search for logging
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getSearchStats(days = 7) {
|
|
129
|
+
const since = Date.now() - (days * 24 * 60 * 60 * 1000);
|
|
130
|
+
return this.db.prepare(`
|
|
131
|
+
SELECT
|
|
132
|
+
method,
|
|
133
|
+
COUNT(*) as count,
|
|
134
|
+
AVG(duration_ms) as avg_duration,
|
|
135
|
+
AVG(results_count) as avg_results
|
|
136
|
+
FROM search_history
|
|
137
|
+
WHERE timestamp > ?
|
|
138
|
+
GROUP BY method
|
|
139
|
+
`).all(since);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Common file tracking operations
|
|
144
|
+
*/
|
|
145
|
+
upsertFile(file) {
|
|
146
|
+
throw new Error('upsertFile must be implemented in subclass');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getFile(filePath) {
|
|
150
|
+
throw new Error('getFile must be implemented in subclass');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isFileIndexed(filePath, contentHash) {
|
|
154
|
+
const file = this.getFile(filePath);
|
|
155
|
+
return file && file.content_hash === contentHash;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
deleteFile(filePath) {
|
|
159
|
+
throw new Error('deleteFile must be implemented in subclass');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Common stats
|
|
164
|
+
*/
|
|
165
|
+
getStats() {
|
|
166
|
+
throw new Error('getStats must be implemented in subclass');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Maintenance
|
|
171
|
+
*/
|
|
172
|
+
vacuum() {
|
|
173
|
+
this.db.exec('VACUUM');
|
|
174
|
+
this.db.exec('ANALYZE');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
close() {
|
|
178
|
+
if (this.db) {
|
|
179
|
+
this.db.close();
|
|
180
|
+
this.isInitialized = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Internal logger
|
|
186
|
+
*/
|
|
187
|
+
_log(message, level = 'info') {
|
|
188
|
+
const prefix = `[${this.constructor.name}]`;
|
|
189
|
+
if (level === 'error') {
|
|
190
|
+
console.error(`${prefix} ${message}`);
|
|
191
|
+
} else if (level === 'warn') {
|
|
192
|
+
console.warn(`${prefix} ${message}`);
|
|
193
|
+
} else {
|
|
194
|
+
console.error(`${prefix} ${message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Safe execute with error handling
|
|
200
|
+
*/
|
|
201
|
+
safeExecute(fn, errorMessage = 'Operation failed') {
|
|
202
|
+
try {
|
|
203
|
+
return fn();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this._log(`${errorMessage}: ${error.message}`, 'error');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Safe async execute with error handling
|
|
212
|
+
*/
|
|
213
|
+
async safeExecuteAsync(fn, errorMessage = 'Operation failed') {
|
|
214
|
+
try {
|
|
215
|
+
return await fn();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this._log(`${errorMessage}: ${error.message}`, 'error');
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default BaseDatabase;
|
|
@@ -77,6 +77,7 @@ export class Chunker {
|
|
|
77
77
|
id: this.generateChunkId(filePath, startLine),
|
|
78
78
|
file_id: this.generateFileId(filePath),
|
|
79
79
|
content: content,
|
|
80
|
+
content_hash: crypto.createHash('md5').update(content).digest('hex'),
|
|
80
81
|
start_line: startLine,
|
|
81
82
|
end_line: endLine,
|
|
82
83
|
token_count: tokens
|