@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.
@@ -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
- await this.watcher.start();
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
- this.codebaseDb?.upsertFile?.(indexed.file);
475
- this.codebaseDb?.deleteFactsByFile?.(indexed.file.id);
476
- this.codebaseDb?.deleteChunksByFile?.(indexed.file.id);
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
- for (const fact of indexed.facts) {
479
- this.codebaseDb?.insertFact?.(fact);
480
- }
560
+ for (const fact of indexed.facts) {
561
+ this.codebaseDb?.insertFact?.(fact);
562
+ }
481
563
 
482
- for (const chunk of indexed.chunks) {
483
- this.codebaseDb?.insertChunk?.(chunk);
484
- }
564
+ for (const chunk of indexed.chunks) {
565
+ this.codebaseDb?.insertChunk?.(chunk);
566
+ }
485
567
 
486
- for (const embedding of indexed.embeddings) {
487
- this.codebaseDb?.insertEmbedding?.(embedding);
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
- // Use getQuery to compile the query properly
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