@mrxkun/mcfast-mcp 4.1.9 → 4.1.11
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/README.md +62 -651
- package/package.json +2 -2
- package/src/index.js +223 -80
- package/src/memory/memory-engine.js +232 -13
- 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 +255 -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: {
|
|
@@ -208,7 +237,13 @@ export class MemoryEngine {
|
|
|
208
237
|
'**/*.log'
|
|
209
238
|
]
|
|
210
239
|
});
|
|
211
|
-
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await this.watcher.start();
|
|
243
|
+
} catch (watcherError) {
|
|
244
|
+
console.error('[MemoryEngine] ⚠️ File watcher failed to start (continuing without file watching):', watcherError.message);
|
|
245
|
+
this.watcher = null;
|
|
246
|
+
}
|
|
212
247
|
|
|
213
248
|
// Perform initial scan
|
|
214
249
|
await this.performInitialScan();
|
|
@@ -223,6 +258,9 @@ export class MemoryEngine {
|
|
|
223
258
|
console.error(`[MemoryEngine] Smart Routing: ${this.smartRoutingEnabled ? 'ENABLED' : 'DISABLED'}`);
|
|
224
259
|
console.error(`[MemoryEngine] Intelligence: ${this.intelligenceEnabled ? 'ENABLED' : 'DISABLED'}`);
|
|
225
260
|
|
|
261
|
+
// Auto-analyze project if MCFAST_TOKEN is set
|
|
262
|
+
await this.autoAnalyzeProject();
|
|
263
|
+
|
|
226
264
|
// Log initialization to daily logs
|
|
227
265
|
await this.dailyLogs.log('Memory Engine Initialized', `Project: ${projectPath}`, {
|
|
228
266
|
stats: this.getStats()
|
|
@@ -264,6 +302,47 @@ export class MemoryEngine {
|
|
|
264
302
|
}
|
|
265
303
|
}
|
|
266
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Auto-analyze project using AI if MCFAST_TOKEN is set
|
|
307
|
+
* Runs in background to not block initialization
|
|
308
|
+
*/
|
|
309
|
+
async autoAnalyzeProject() {
|
|
310
|
+
if (!this.autoAnalyze) return;
|
|
311
|
+
|
|
312
|
+
const apiKey = process.env.MCFAST_TOKEN;
|
|
313
|
+
if (!apiKey) {
|
|
314
|
+
console.error('[MemoryEngine] Skipping auto-analysis (no MCFAST_TOKEN)');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check if already analyzed (MEMORY.md has Project Context section)
|
|
319
|
+
try {
|
|
320
|
+
const memoryContent = await this.curatedMemory.read();
|
|
321
|
+
if (memoryContent && memoryContent.includes('## Project Context') &&
|
|
322
|
+
!memoryContent.includes('<!-- Add project context here -->')) {
|
|
323
|
+
console.error('[MemoryEngine] Project already analyzed, skipping auto-analysis');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
// MEMORY.md doesn't exist yet, proceed with analysis
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Run analysis in background
|
|
331
|
+
console.error('[MemoryEngine] 🔄 Starting auto-analysis in background...');
|
|
332
|
+
|
|
333
|
+
projectAnalyzeExecute({ force: false, updateMemory: true })
|
|
334
|
+
.then(result => {
|
|
335
|
+
if (result.metadata?.memoryUpdated) {
|
|
336
|
+
console.error('[MemoryEngine] ✅ Auto-analysis complete, MEMORY.md updated');
|
|
337
|
+
} else if (result.isError) {
|
|
338
|
+
console.error('[MemoryEngine] ⚠️ Auto-analysis failed:', result.content?.[0]?.text?.substring(0, 100));
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.catch(err => {
|
|
342
|
+
console.error('[MemoryEngine] ⚠️ Auto-analysis error:', err.message);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
267
346
|
async performInitialScan() {
|
|
268
347
|
if (this.isScanning) return;
|
|
269
348
|
this.isScanning = true;
|
|
@@ -470,24 +549,44 @@ export class MemoryEngine {
|
|
|
470
549
|
|
|
471
550
|
// ========== Storage Operations ==========
|
|
472
551
|
|
|
473
|
-
async storeIndexed(indexed) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
552
|
+
async storeIndexed(indexed, maxRetries = 3) {
|
|
553
|
+
// Retry logic with exponential backoff
|
|
554
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
555
|
+
try {
|
|
556
|
+
this.codebaseDb?.upsertFile?.(indexed.file);
|
|
557
|
+
this.codebaseDb?.deleteFactsByFile?.(indexed.file.id);
|
|
558
|
+
this.codebaseDb?.deleteChunksByFile?.(indexed.file.id);
|
|
477
559
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
560
|
+
for (const fact of indexed.facts) {
|
|
561
|
+
this.codebaseDb?.insertFact?.(fact);
|
|
562
|
+
}
|
|
481
563
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
564
|
+
for (const chunk of indexed.chunks) {
|
|
565
|
+
this.codebaseDb?.insertChunk?.(chunk);
|
|
566
|
+
}
|
|
485
567
|
|
|
486
|
-
|
|
487
|
-
|
|
568
|
+
for (const embedding of indexed.embeddings) {
|
|
569
|
+
this.codebaseDb?.insertEmbedding?.(embedding);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return true; // Success
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (attempt === maxRetries) {
|
|
575
|
+
console.error(`[MemoryEngine] Failed to store indexed data after ${maxRetries} attempts:`, error.message);
|
|
576
|
+
this.codebaseDb?.recordFailedIndex?.(indexed.file.path, error.message);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
// Exponential backoff: 100ms, 200ms, 400ms
|
|
580
|
+
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
|
|
581
|
+
}
|
|
488
582
|
}
|
|
489
583
|
}
|
|
490
584
|
|
|
585
|
+
// Record failed indexing for tracking
|
|
586
|
+
recordFailedIndex(filePath, errorMessage) {
|
|
587
|
+
console.error(`[MemoryEngine] Indexing failed for ${filePath}: ${errorMessage}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
491
590
|
// ========== Search Operations ==========
|
|
492
591
|
|
|
493
592
|
async searchFacts(query, limit = 20) {
|
|
@@ -573,6 +672,80 @@ export class MemoryEngine {
|
|
|
573
672
|
}
|
|
574
673
|
|
|
575
674
|
async searchCodebase(query, limit = 20) {
|
|
675
|
+
const startTime = performance.now();
|
|
676
|
+
|
|
677
|
+
// FIX: Get embeddings from both codebaseDb AND memoryDb
|
|
678
|
+
const [codebaseEmbeddings, memoryEmbeddings] = await Promise.all([
|
|
679
|
+
this.codebaseDb?.getAllEmbeddings?.() || [],
|
|
680
|
+
this.memoryDb?.getAllEmbeddings?.() || []
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
// Generate query embedding
|
|
684
|
+
const queryResult = this.embedder.embedCode(query);
|
|
685
|
+
const queryEmbedding = queryResult.vector;
|
|
686
|
+
|
|
687
|
+
// Score and tag results from each source
|
|
688
|
+
const scoreFn = (item) => {
|
|
689
|
+
const buf = item.embedding?.buffer || item.embedding;
|
|
690
|
+
const arr = buf ? new Uint8Array(buf) : new Uint8Array(0);
|
|
691
|
+
return this.embedder.cosineSimilarity(queryEmbedding, Array.from(arr));
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const codebaseResults = (codebaseEmbeddings || []).map(item => ({
|
|
695
|
+
...item, similarity: scoreFn(item), source: 'codebase', type: 'code'
|
|
696
|
+
}));
|
|
697
|
+
|
|
698
|
+
const memoryResults = (memoryEmbeddings || []).map(item => ({
|
|
699
|
+
...item, similarity: scoreFn(item), source: 'memory', type: 'memory'
|
|
700
|
+
}));
|
|
701
|
+
|
|
702
|
+
// Combine results
|
|
703
|
+
let combined = [...codebaseResults, ...memoryResults];
|
|
704
|
+
|
|
705
|
+
// Also get FTS results from both databases
|
|
706
|
+
const [codebaseFts, memoryFts] = await Promise.all([
|
|
707
|
+
this.codebaseDb?.searchFTS?.(query, limit * 2) || { results: [] },
|
|
708
|
+
this.memoryDb?.searchFTS?.(query, limit * 2) || { results: [] }
|
|
709
|
+
]);
|
|
710
|
+
|
|
711
|
+
// Add FTS results
|
|
712
|
+
for (const r of codebaseFts.results || []) {
|
|
713
|
+
combined.push({ ...r, source: 'fts', type: 'code', score: r.score });
|
|
714
|
+
}
|
|
715
|
+
for (const r of memoryFts.results || []) {
|
|
716
|
+
combined.push({ ...r, source: 'fts', type: 'memory', score: r.score });
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Deduplicate and sort
|
|
720
|
+
const seen = new Map();
|
|
721
|
+
for (const r of combined) {
|
|
722
|
+
const key = r.chunk_id || r.id;
|
|
723
|
+
if (!seen.has(key) || (r.similarity || r.score || 0) > (seen.get(key).similarity || seen.get(key).score || 0)) {
|
|
724
|
+
seen.set(key, r);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const finalResults = Array.from(seen.values())
|
|
729
|
+
.sort((a, b) => (b.similarity || b.score || 0) - (a.similarity || a.score || 0))
|
|
730
|
+
.slice(0, limit);
|
|
731
|
+
|
|
732
|
+
const duration = performance.now() - startTime;
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
results: finalResults,
|
|
736
|
+
metadata: {
|
|
737
|
+
vectorCandidates: codebaseResults.length + memoryResults.length,
|
|
738
|
+
ftsCandidates: (codebaseFts.results?.length || 0) + (memoryFts.results?.length || 0),
|
|
739
|
+
totalCandidates: combined.length,
|
|
740
|
+
codebaseResults: codebaseResults.length,
|
|
741
|
+
memoryResults: memoryResults.length,
|
|
742
|
+
duration: duration.toFixed(2) + 'ms'
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Legacy search - kept for backward compatibility
|
|
748
|
+
async _searchCodebaseLegacy(query, limit = 20) {
|
|
576
749
|
// Search codebase using existing methods
|
|
577
750
|
const vectorResults = await this.searchVector(query, limit * 2);
|
|
578
751
|
const ftsResults = this.codebaseDb?.searchFTS?.(query, limit * 2);
|
|
@@ -731,6 +904,25 @@ export class MemoryEngine {
|
|
|
731
904
|
return { strategy: 'cached', confidence: 0.6, reason: 'Default selection' };
|
|
732
905
|
}
|
|
733
906
|
|
|
907
|
+
async getCuratedMemories() {
|
|
908
|
+
return this.curatedMemory?.read?.() || null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
getIntelligenceStats() {
|
|
912
|
+
return {
|
|
913
|
+
enabled: this.intelligenceEnabled,
|
|
914
|
+
patternDetector: {
|
|
915
|
+
totalPatterns: this.patternDetector?.getTotalPatterns?.() || 0
|
|
916
|
+
},
|
|
917
|
+
suggestionEngine: {
|
|
918
|
+
minConfidence: this.suggestionEngine?.minConfidence || 0.7
|
|
919
|
+
},
|
|
920
|
+
strategySelector: {
|
|
921
|
+
learningRate: this.strategySelector?.learningRate || 0.1
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
734
926
|
// ========== Sync Engine ==========
|
|
735
927
|
|
|
736
928
|
getSyncStatus() {
|
|
@@ -762,6 +954,33 @@ export class MemoryEngine {
|
|
|
762
954
|
this.isInitialized = false;
|
|
763
955
|
console.error('[MemoryEngine] Shutdown complete');
|
|
764
956
|
}
|
|
957
|
+
|
|
958
|
+
async cleanup() {
|
|
959
|
+
// Alias for shutdown for consistency
|
|
960
|
+
return this.shutdown();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Auto-register cleanup handlers for graceful shutdown
|
|
965
|
+
if (typeof process !== 'undefined') {
|
|
966
|
+
const cleanupHandler = async (signal) => {
|
|
967
|
+
console.error(`[MemoryEngine] Received ${signal}, performing cleanup...`);
|
|
968
|
+
if (MemoryEngine.instance) {
|
|
969
|
+
try {
|
|
970
|
+
await MemoryEngine.instance.shutdown();
|
|
971
|
+
} catch (error) {
|
|
972
|
+
console.error(`[MemoryEngine] Error during shutdown: ${error.message}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// Register handlers only once
|
|
978
|
+
if (!process._memoryEngineCleanupRegistered) {
|
|
979
|
+
process.on('beforeExit', () => cleanupHandler('beforeExit'));
|
|
980
|
+
process.on('SIGINT', () => cleanupHandler('SIGINT'));
|
|
981
|
+
process.on('SIGTERM', () => cleanupHandler('SIGTERM'));
|
|
982
|
+
process._memoryEngineCleanupRegistered = true;
|
|
983
|
+
}
|
|
765
984
|
}
|
|
766
985
|
|
|
767
986
|
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
|
|
@@ -59,12 +59,12 @@ export class CodeIndexer {
|
|
|
59
59
|
const fileId = this.generateFileId(filePath);
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
|
-
//
|
|
62
|
+
// Try tree-sitter query first (for functions)
|
|
63
63
|
const query = await getQuery(language, 'definitions');
|
|
64
|
-
|
|
65
|
-
if (query) {
|
|
64
|
+
|
|
65
|
+
if (query && ast?.rootNode) {
|
|
66
66
|
const captures = query.captures(ast.rootNode);
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
for (const capture of captures) {
|
|
69
69
|
if (capture.name === 'name' || capture.name === 'function') {
|
|
70
70
|
facts.push({
|
|
@@ -81,6 +81,95 @@ export class CodeIndexer {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
// Fallback/enhancement: Extract more facts using regex patterns
|
|
86
|
+
// This works across languages and captures additional symbols
|
|
87
|
+
const content = ast?.text || '';
|
|
88
|
+
|
|
89
|
+
// Extract classes
|
|
90
|
+
const classMatches = content.matchAll(/class\s+(\w+)(?:\s+extends\s+(\w+))?/g);
|
|
91
|
+
for (const match of classMatches) {
|
|
92
|
+
if (!facts.find(f => f.type === 'class' && f.name === match[1])) {
|
|
93
|
+
facts.push({
|
|
94
|
+
id: this.generateFactId(fileId, match[1], 'class'),
|
|
95
|
+
file_id: fileId,
|
|
96
|
+
type: 'class',
|
|
97
|
+
name: match[1],
|
|
98
|
+
line_start: this.findLineNumber(content, match[0]) || 0,
|
|
99
|
+
line_end: 0,
|
|
100
|
+
signature: match[2] ? `extends ${match[2]}` : '',
|
|
101
|
+
exported: this.isExported(content, match[1]),
|
|
102
|
+
confidence: 0.9
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Extract interfaces (TypeScript)
|
|
108
|
+
if (language === 'typescript') {
|
|
109
|
+
const interfaceMatches = content.matchAll(/interface\s+(\w+)(?:\s+extends\s+([^{]+))?/g);
|
|
110
|
+
for (const match of interfaceMatches) {
|
|
111
|
+
if (!facts.find(f => f.type === 'interface' && f.name === match[1])) {
|
|
112
|
+
facts.push({
|
|
113
|
+
id: this.generateFactId(fileId, match[1], 'interface'),
|
|
114
|
+
file_id: fileId,
|
|
115
|
+
type: 'interface',
|
|
116
|
+
name: match[1],
|
|
117
|
+
line_start: this.findLineNumber(content, match[0]) || 0,
|
|
118
|
+
line_end: 0,
|
|
119
|
+
signature: match[2] ? `extends ${match[2].trim()}` : '',
|
|
120
|
+
exported: this.isExported(content, match[1]),
|
|
121
|
+
confidence: 0.9
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract imports
|
|
128
|
+
const importMatches = content.matchAll(/import\s+(?:(\w+)|\{\s*([^}]+)\s*\}|\*\s+as\s+(\w+))\s+from\s+['"]([^'"]+)['"]/g);
|
|
129
|
+
for (const match of importMatches) {
|
|
130
|
+
const imported = match[1] || match[2]?.split(',').map(s => s.trim().split(' ')[0]).join(', ') || match[3];
|
|
131
|
+
facts.push({
|
|
132
|
+
id: this.generateFactId(fileId, imported, 'import'),
|
|
133
|
+
file_id: fileId,
|
|
134
|
+
type: 'import',
|
|
135
|
+
name: imported,
|
|
136
|
+
line_start: this.findLineNumber(content, match[0]) || 0,
|
|
137
|
+
line_end: 0,
|
|
138
|
+
signature: match[4],
|
|
139
|
+
exported: false,
|
|
140
|
+
confidence: 1.0
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract exports
|
|
145
|
+
const exportMatches = content.matchAll(/export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type)\s+(\w+)/g);
|
|
146
|
+
for (const match of exportMatches) {
|
|
147
|
+
if (!facts.find(f => f.name === match[1] && f.exported)) {
|
|
148
|
+
const existing = facts.find(f => f.name === match[1]);
|
|
149
|
+
if (existing) {
|
|
150
|
+
existing.exported = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Extract type definitions (TypeScript)
|
|
156
|
+
if (language === 'typescript') {
|
|
157
|
+
const typeMatches = content.matchAll(/type\s+(\w+)\s*=/g);
|
|
158
|
+
for (const match of typeMatches) {
|
|
159
|
+
facts.push({
|
|
160
|
+
id: this.generateFactId(fileId, match[1], 'type'),
|
|
161
|
+
file_id: fileId,
|
|
162
|
+
type: 'type',
|
|
163
|
+
name: match[1],
|
|
164
|
+
line_start: this.findLineNumber(content, match[0]) || 0,
|
|
165
|
+
line_end: 0,
|
|
166
|
+
signature: 'type alias',
|
|
167
|
+
exported: this.isExported(content, match[1]),
|
|
168
|
+
confidence: 0.8
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
84
173
|
} catch (error) {
|
|
85
174
|
console.warn(`[Indexer] Failed to extract facts:`, error.message);
|
|
86
175
|
}
|
|
@@ -88,6 +177,23 @@ export class CodeIndexer {
|
|
|
88
177
|
return facts;
|
|
89
178
|
}
|
|
90
179
|
|
|
180
|
+
// Helper to find line number of a string in content
|
|
181
|
+
findLineNumber(content, searchStr) {
|
|
182
|
+
const lines = content.split('\n');
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
if (lines[i].includes(searchStr)) {
|
|
185
|
+
return i + 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if a symbol is exported
|
|
192
|
+
isExported(content, symbolName) {
|
|
193
|
+
return new RegExp(`export\\s+.*\\b${symbolName}\\b`).test(content) ||
|
|
194
|
+
new RegExp(`export\\s+default`).test(content);
|
|
195
|
+
}
|
|
196
|
+
|
|
91
197
|
async generateEmbeddings(chunks, language = 'javascript') {
|
|
92
198
|
const embeddings = [];
|
|
93
199
|
|