@mrxkun/mcfast-mcp 4.1.10 → 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.
@@ -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
 
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Logger Utility for Memory System
3
+ * Provides structured logging with different levels
4
+ *
5
+ * Features:
6
+ * - Log levels: debug, info, warn, error
7
+ * - Timestamp formatting
8
+ * - Module context
9
+ * - Configurable output
10
+ */
11
+
12
+ const LOG_LEVELS = {
13
+ debug: 0,
14
+ info: 1,
15
+ warn: 2,
16
+ error: 3
17
+ };
18
+
19
+ class Logger {
20
+ constructor(options = {}) {
21
+ this.module = options.module || 'Memory';
22
+ this.level = options.level || 'info';
23
+ this.prefix = options.prefix || '';
24
+
25
+ // Set minimum level
26
+ this.minLevel = LOG_LEVELS[options.minLevel?.toLowerCase()] ?? LOG_LEVELS.info;
27
+ }
28
+
29
+ /**
30
+ * Set log level
31
+ */
32
+ setLevel(level) {
33
+ this.minLevel = LOG_LEVELS[level?.toLowerCase()] ?? LOG_LEVELS.info;
34
+ }
35
+
36
+ /**
37
+ * Format timestamp
38
+ */
39
+ _formatTime() {
40
+ return new Date().toISOString();
41
+ }
42
+
43
+ /**
44
+ * Format log message
45
+ */
46
+ _format(level, message, data = null) {
47
+ const timestamp = this._formatTime();
48
+ const context = this.prefix ? `[${this.prefix}]` : `[${this.module}]`;
49
+
50
+ let formatted = `${timestamp} ${context} ${level.toUpperCase()}: ${message}`;
51
+
52
+ if (data) {
53
+ if (data instanceof Error) {
54
+ formatted += `\n Stack: ${data.stack}`;
55
+ } else if (typeof data === 'object') {
56
+ formatted += `\n Data: ${JSON.stringify(data, null, 2)}`;
57
+ } else {
58
+ formatted += `\n ${data}`;
59
+ }
60
+ }
61
+
62
+ return formatted;
63
+ }
64
+
65
+ /**
66
+ * Debug level logging
67
+ */
68
+ debug(message, data = null) {
69
+ if (this.minLevel > LOG_LEVELS.debug) return;
70
+ console.error(this._format('debug', message, data));
71
+ }
72
+
73
+ /**
74
+ * Info level logging
75
+ */
76
+ info(message, data = null) {
77
+ if (this.minLevel > LOG_LEVELS.info) return;
78
+ console.error(this._format('info', message, data));
79
+ }
80
+
81
+ /**
82
+ * Warn level logging
83
+ */
84
+ warn(message, data = null) {
85
+ if (this.minLevel > LOG_LEVELS.warn) return;
86
+ console.error(this._format('warn', message, data));
87
+ }
88
+
89
+ /**
90
+ * Error level logging
91
+ */
92
+ error(message, data = null) {
93
+ if (this.minLevel > LOG_LEVELS.error) return;
94
+ console.error(this._format('error', message, data));
95
+ }
96
+
97
+ /**
98
+ * Create child logger with additional context
99
+ */
100
+ child(options = {}) {
101
+ return new Logger({
102
+ ...options,
103
+ module: this.module,
104
+ minLevel: Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === this.minLevel)
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Timer for performance tracking
110
+ */
111
+ timer(label = 'operation') {
112
+ return new Timer(this, label);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Timer class for performance tracking
118
+ */
119
+ class Timer {
120
+ constructor(logger, label) {
121
+ this.logger = logger;
122
+ this.label = label;
123
+ this.start = performance.now();
124
+ }
125
+
126
+ end() {
127
+ const duration = performance.now() - this.start;
128
+ this.logger.debug(`${this.label} completed in ${duration.toFixed(2)}ms`);
129
+ return duration;
130
+ }
131
+ }
132
+
133
+ // Pre-configured loggers for common modules
134
+ export const loggers = {
135
+ memory: new Logger({ module: 'MemoryEngine', minLevel: 'info' }),
136
+ watcher: new Logger({ module: 'FileWatcher', minLevel: 'info' }),
137
+ indexer: new Logger({ module: 'Indexer', minLevel: 'info' }),
138
+ database: new Logger({ module: 'Database', minLevel: 'info' }),
139
+ embedder: new Logger({ module: 'Embedder', minLevel: 'info' }),
140
+ intelligence: new Logger({ module: 'Intelligence', minLevel: 'info' })
141
+ };
142
+
143
+ /**
144
+ * Get logger for specific module
145
+ */
146
+ export function getLogger(module, options = {}) {
147
+ if (loggers[module]) {
148
+ return loggers[module].child(options);
149
+ }
150
+ return new Logger({ module, ...options });
151
+ }
152
+
153
+ /**
154
+ * Set global log level
155
+ */
156
+ export function setGlobalLevel(level) {
157
+ Object.values(loggers).forEach(logger => {
158
+ logger.setLevel(level);
159
+ });
160
+ }
161
+
162
+ export default Logger;