@grec0/memory-bank-mcp 0.0.3 → 0.0.5

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.
@@ -6,8 +6,12 @@ import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { scanFiles, scanSingleFile } from "./fileScanner.js";
8
8
  import { chunkCode } from "./chunker.js";
9
- import { logger } from "./logger.js";
10
- import * as crypto from "crypto";
9
+ /**
10
+ * Normalizes a path to use forward slashes for consistent cross-platform behavior
11
+ */
12
+ function normalizePath(filePath) {
13
+ return filePath.split(path.sep).join('/');
14
+ }
11
15
  /**
12
16
  * Index manager coordinating the entire indexing pipeline
13
17
  */
@@ -16,34 +20,49 @@ export class IndexManager {
16
20
  vectorStore;
17
21
  metadataPath;
18
22
  metadata;
19
- projectRoot;
20
- projectId;
21
- constructor(embeddingService, vectorStore, storagePath = ".memorybank", projectRoot) {
23
+ projectKnowledgeService = null;
24
+ autoUpdateDocs = false;
25
+ constructor(embeddingService, vectorStore, storagePath = ".memorybank") {
22
26
  this.embeddingService = embeddingService;
23
27
  this.vectorStore = vectorStore;
24
28
  this.metadataPath = path.join(storagePath, "index-metadata.json");
25
- this.projectRoot = projectRoot || process.cwd();
26
- this.projectId = this.generateProjectId(this.projectRoot);
27
29
  this.metadata = this.loadMetadata();
30
+ // Check if auto-update docs is enabled via environment variable
31
+ this.autoUpdateDocs = process.env.MEMORYBANK_AUTO_UPDATE_DOCS === "true";
32
+ }
33
+ /**
34
+ * Sets the Project Knowledge Service for auto-generating docs
35
+ */
36
+ setProjectKnowledgeService(service) {
37
+ this.projectKnowledgeService = service;
38
+ console.error("Project Knowledge Service attached to Index Manager");
28
39
  }
29
40
  /**
30
- * Generates a unique project ID from the project root path
41
+ * Enables or disables auto-update of project docs after indexing
31
42
  */
32
- generateProjectId(projectRoot) {
33
- return crypto.createHash("sha256").update(projectRoot).digest("hex").substring(0, 16);
43
+ setAutoUpdateDocs(enabled) {
44
+ this.autoUpdateDocs = enabled;
45
+ console.error(`Auto-update project docs: ${enabled ? "enabled" : "disabled"}`);
34
46
  }
35
47
  /**
36
48
  * Loads index metadata from disk
37
49
  */
38
50
  loadMetadata() {
39
51
  try {
52
+ console.error(`Loading metadata from: ${this.metadataPath}`);
40
53
  if (fs.existsSync(this.metadataPath)) {
41
54
  const data = fs.readFileSync(this.metadataPath, "utf-8");
42
- return JSON.parse(data);
55
+ const metadata = JSON.parse(data);
56
+ const fileCount = Object.keys(metadata.files || {}).length;
57
+ console.error(`Loaded metadata: ${fileCount} files tracked`);
58
+ return metadata;
59
+ }
60
+ else {
61
+ console.error(`Metadata file not found: ${this.metadataPath}`);
43
62
  }
44
63
  }
45
64
  catch (error) {
46
- logger.warn(`Could not load index metadata: ${error}`);
65
+ console.error(`Warning: Could not load index metadata: ${error}`);
47
66
  }
48
67
  return {
49
68
  version: "1.0",
@@ -60,10 +79,12 @@ export class IndexManager {
60
79
  if (!fs.existsSync(dir)) {
61
80
  fs.mkdirSync(dir, { recursive: true });
62
81
  }
82
+ const fileCount = Object.keys(this.metadata.files).length;
63
83
  fs.writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2));
84
+ console.error(` Saved metadata: ${fileCount} files tracked → ${this.metadataPath}`);
64
85
  }
65
86
  catch (error) {
66
- logger.warn(`Could not save index metadata: ${error}`);
87
+ console.error(`Warning: Could not save index metadata: ${error}`);
67
88
  }
68
89
  }
69
90
  /**
@@ -73,11 +94,21 @@ export class IndexManager {
73
94
  if (forceReindex) {
74
95
  return true;
75
96
  }
76
- const fileInfo = this.metadata.files[file.path];
97
+ // Use normalized path for consistent lookup
98
+ const normalizedPath = normalizePath(file.path);
99
+ const fileInfo = this.metadata.files[normalizedPath];
77
100
  if (!fileInfo) {
101
+ // Debug: show first few new files
102
+ const trackedCount = Object.keys(this.metadata.files).length;
103
+ if (trackedCount === 0) {
104
+ console.error(` New file (no metadata): ${normalizedPath}`);
105
+ }
78
106
  return true; // New file
79
107
  }
80
108
  if (fileInfo.hash !== file.hash) {
109
+ console.error(` Changed file: ${normalizedPath}`);
110
+ console.error(` Stored hash: ${fileInfo.hash.substring(0, 16)}...`);
111
+ console.error(` Current hash: ${file.hash.substring(0, 16)}...`);
81
112
  return true; // File changed
82
113
  }
83
114
  return false;
@@ -85,106 +116,142 @@ export class IndexManager {
85
116
  /**
86
117
  * Indexes a single file
87
118
  */
88
- async indexFile(file, forceReindex = false, saveMetadata = true) {
119
+ async indexFile(file, forceReindex = false, projectId = "default") {
89
120
  try {
90
121
  // Check if file needs reindexing
91
122
  if (!this.needsReindexing(file, forceReindex)) {
92
- logger.debug(`Skipping ${file.path} (no changes)`);
123
+ console.error(`Skipping ${file.path} (no changes)`);
93
124
  return { chunksCreated: 0 };
94
125
  }
95
- logger.info(`Indexing: ${file.path}`);
126
+ console.error(`Indexing: ${file.path}`);
96
127
  // Read file content
97
128
  const content = fs.readFileSync(file.absolutePath, "utf-8");
98
- // Get chunk size from environment or use defaults
99
- const maxChunkSize = parseInt(process.env.MEMORYBANK_CHUNK_SIZE || "1000");
100
- const chunkOverlap = parseInt(process.env.MEMORYBANK_CHUNK_OVERLAP || "200");
101
- // Chunk the code
129
+ // Get token limits from environment or use defaults
130
+ // text-embedding-3-small has 8192 token limit, default to 7500 for safety
131
+ const maxTokens = parseInt(process.env.MEMORYBANK_MAX_TOKENS || "7500");
132
+ const chunkOverlapTokens = parseInt(process.env.MEMORYBANK_CHUNK_OVERLAP_TOKENS || "200");
133
+ // Chunk the code using token-based chunking
102
134
  const chunks = chunkCode({
103
135
  filePath: file.path,
104
136
  content,
105
137
  language: file.language,
106
- maxChunkSize,
107
- chunkOverlap,
138
+ maxTokens,
139
+ chunkOverlapTokens,
108
140
  });
109
141
  if (chunks.length === 0) {
110
- logger.warn(`No chunks created for ${file.path}`);
111
- return { chunksCreated: 0 };
112
- }
113
- logger.debug(` Created ${chunks.length} chunks`);
114
- // Filter out invalid chunks (fail-safe)
115
- const validChunks = chunks.filter(c => c.content && c.content.trim().length > 0 && c.content.trim() !== "}");
116
- if (validChunks.length === 0) {
117
- logger.warn(`No valid chunks after filtering for ${file.path}`);
142
+ console.error(`Warning: No chunks created for ${file.path}`);
118
143
  return { chunksCreated: 0 };
119
144
  }
145
+ console.error(` Created ${chunks.length} chunks`);
120
146
  // Generate embeddings
121
- const embeddingInputs = validChunks.map((chunk) => ({
147
+ const embeddingInputs = chunks.map((chunk) => ({
122
148
  id: chunk.id,
123
149
  content: chunk.content,
124
150
  }));
125
151
  const embeddings = await this.embeddingService.generateBatchEmbeddings(embeddingInputs);
126
- logger.debug(` Generated ${embeddings.length} embeddings`);
127
- // Prepare chunk records for storage
152
+ console.error(` Generated ${embeddings.length} embeddings`);
153
+ // Prepare chunk records for storage (using snake_case for LanceDB)
154
+ // Note: All fields must have non-undefined values for LanceDB Arrow conversion
128
155
  const timestamp = Date.now();
129
- const chunkRecords = validChunks.map((chunk, i) => ({
156
+ const normalizedFilePath = normalizePath(file.path);
157
+ console.error(` Storing chunks with project_id: '${projectId}', file: '${normalizedFilePath}'`);
158
+ const chunkRecords = chunks.map((chunk, i) => ({
130
159
  id: chunk.id,
131
160
  vector: embeddings[i].vector,
132
- filePath: chunk.filePath,
161
+ file_path: normalizedFilePath, // Use normalized path for consistency
133
162
  content: chunk.content,
134
- startLine: chunk.startLine,
135
- endLine: chunk.endLine,
136
- chunkType: chunk.chunkType,
137
- name: chunk.name || "",
163
+ start_line: chunk.startLine,
164
+ end_line: chunk.endLine,
165
+ chunk_type: chunk.chunkType,
166
+ name: chunk.name || "", // Ensure non-undefined for LanceDB
138
167
  language: chunk.language,
139
- fileHash: file.hash,
168
+ file_hash: file.hash,
140
169
  timestamp,
141
- context: chunk.context,
142
- projectId: this.projectId,
170
+ context: chunk.context || "", // Ensure non-undefined for LanceDB
171
+ project_id: projectId,
143
172
  }));
144
- // Delete old chunks for this file
145
- await this.vectorStore.deleteChunksByFile(file.path, this.projectId);
173
+ // Delete old chunks for this file using normalized path
174
+ await this.vectorStore.deleteChunksByFile(normalizedFilePath);
146
175
  // Insert new chunks
147
176
  await this.vectorStore.insertChunks(chunkRecords);
148
- logger.debug(` Stored ${chunkRecords.length} chunks in vector store`);
149
- // Update metadata
150
- this.metadata.files[file.path] = {
177
+ console.error(` Stored ${chunkRecords.length} chunks in vector store`);
178
+ // Update metadata with normalized path for consistent lookups
179
+ this.metadata.files[normalizedFilePath] = {
151
180
  hash: file.hash,
152
181
  lastIndexed: timestamp,
153
182
  chunkCount: chunks.length,
154
183
  };
155
184
  this.metadata.lastIndexed = timestamp;
156
- if (saveMetadata) {
157
- this.saveMetadata();
158
- }
185
+ this.saveMetadata();
159
186
  return { chunksCreated: chunks.length };
160
187
  }
161
188
  catch (error) {
162
189
  const errorMsg = `Error indexing ${file.path}: ${error}`;
163
- logger.error(errorMsg);
190
+ console.error(errorMsg);
164
191
  return { chunksCreated: 0, error: errorMsg };
165
192
  }
166
193
  }
194
+ /**
195
+ * Derives a project ID from the root path if not provided
196
+ */
197
+ deriveProjectId(rootPath, providedId) {
198
+ if (providedId) {
199
+ return providedId;
200
+ }
201
+ // Use the directory name as project ID
202
+ const dirName = path.basename(path.resolve(rootPath));
203
+ // Sanitize: remove special chars, lowercase, replace spaces with dashes
204
+ const sanitized = dirName
205
+ .toLowerCase()
206
+ .replace(/[^a-z0-9-_]/g, "-")
207
+ .replace(/-+/g, "-")
208
+ .replace(/^-|-$/g, "");
209
+ return sanitized || "default";
210
+ }
167
211
  /**
168
212
  * Indexes multiple files or a directory
169
213
  */
170
214
  async indexFiles(options) {
171
215
  const startTime = Date.now();
172
- logger.info(`=== Starting indexing process ===`);
173
- logger.info(`Root path: ${options.rootPath}`);
174
- logger.info(`Force reindex: ${options.forceReindex || false}`);
216
+ const projectId = this.deriveProjectId(options.rootPath, options.projectId);
217
+ const shouldAutoUpdateDocs = options.autoUpdateDocs !== undefined
218
+ ? options.autoUpdateDocs
219
+ : this.autoUpdateDocs;
220
+ // Use workspaceRoot for consistent path normalization, fallback to rootPath
221
+ const workspaceRoot = options.workspaceRoot || options.rootPath;
222
+ console.error(`\n=== Starting indexing process ===`);
223
+ console.error(`Root path: ${options.rootPath}`);
224
+ console.error(`Workspace root: ${workspaceRoot}`);
225
+ console.error(`Project ID: ${projectId}`);
226
+ console.error(`Force reindex: ${options.forceReindex || false}`);
227
+ console.error(`Auto-update docs: ${shouldAutoUpdateDocs}`);
175
228
  // Initialize vector store
176
229
  await this.vectorStore.initialize();
177
- // Scan files
178
- logger.info(`Scanning files...`);
179
- const files = await scanFiles({
230
+ // Scan files - always use workspaceRoot for consistent relative paths
231
+ console.error(`\nScanning files...`);
232
+ const files = scanFiles({
180
233
  rootPath: options.rootPath,
181
- projectRoot: options.projectRoot,
182
234
  recursive: options.recursive !== undefined ? options.recursive : true,
183
235
  });
236
+ // Normalize paths to be relative to workspaceRoot, not rootPath
237
+ // This ensures consistent paths in metadata regardless of which subfolder was indexed
238
+ if (workspaceRoot !== options.rootPath) {
239
+ const rootPathResolved = path.resolve(options.rootPath);
240
+ const workspaceRootResolved = path.resolve(workspaceRoot);
241
+ for (const file of files) {
242
+ // Convert path from relative-to-rootPath to relative-to-workspaceRoot
243
+ const absolutePath = path.join(rootPathResolved, file.path);
244
+ file.path = path.relative(workspaceRootResolved, absolutePath);
245
+ // Use forward slashes for consistency
246
+ file.path = file.path.split(path.sep).join('/');
247
+ }
248
+ console.error(` Normalized ${files.length} file paths to workspace root`);
249
+ }
184
250
  if (files.length === 0) {
185
- logger.warn("No files found to index");
251
+ console.error("No files found to index");
186
252
  return {
187
253
  filesProcessed: 0,
254
+ changedFiles: [],
188
255
  chunksCreated: 0,
189
256
  errors: [],
190
257
  duration: Date.now() - startTime,
@@ -192,69 +259,76 @@ export class IndexManager {
192
259
  }
193
260
  // Filter files that need reindexing
194
261
  const filesToIndex = files.filter((file) => this.needsReindexing(file, options.forceReindex || false));
195
- logger.info(`Found ${files.length} files, ${filesToIndex.length} need indexing`);
262
+ console.error(`\nFound ${files.length} files, ${filesToIndex.length} need indexing`);
196
263
  if (filesToIndex.length === 0) {
197
- logger.info("All files are up to date");
264
+ console.error("All files are up to date");
198
265
  return {
199
266
  filesProcessed: 0,
267
+ changedFiles: [],
200
268
  chunksCreated: 0,
201
269
  errors: [],
202
270
  duration: Date.now() - startTime,
203
271
  };
204
272
  }
205
- // Index files in batches
273
+ // Index files
206
274
  const errors = [];
275
+ const changedFiles = [];
207
276
  let totalChunks = 0;
208
277
  let processedFiles = 0;
209
- const batchSize = 5; // Concurrency limit
210
- for (let i = 0; i < filesToIndex.length; i += batchSize) {
211
- const batch = filesToIndex.slice(i, i + batchSize);
212
- const batchNum = Math.floor(i / batchSize) + 1;
213
- const totalBatches = Math.ceil(filesToIndex.length / batchSize);
214
- logger.info(`Processing batch ${batchNum}/${totalBatches} (${batch.length} files)`);
215
- const batchPromises = batch.map(async (file, index) => {
216
- logger.debug(`[${i + index + 1}/${filesToIndex.length}] Processing ${file.path}`);
217
- return this.indexFile(file, options.forceReindex || false, false); // Don't save metadata per file
218
- });
219
- const results = await Promise.all(batchPromises);
220
- // Process results
221
- for (const result of results) {
222
- if (result.error) {
223
- errors.push(result.error);
224
- }
225
- else {
226
- processedFiles++;
227
- totalChunks += result.chunksCreated;
228
- }
278
+ for (let i = 0; i < filesToIndex.length; i++) {
279
+ const file = filesToIndex[i];
280
+ console.error(`\n[${i + 1}/${filesToIndex.length}] Processing ${file.path}`);
281
+ const result = await this.indexFile(file, options.forceReindex || false, projectId);
282
+ if (result.error) {
283
+ errors.push(result.error);
229
284
  }
230
- // Save metadata and embedding cache after each batch
231
- this.saveMetadata();
232
- this.embeddingService.saveCache();
233
- // Small delay between batches
234
- if (i + batchSize < filesToIndex.length) {
235
- await new Promise(resolve => setTimeout(resolve, 100));
285
+ else {
286
+ processedFiles++;
287
+ totalChunks += result.chunksCreated;
288
+ changedFiles.push(file.path);
289
+ }
290
+ }
291
+ const indexDuration = Date.now() - startTime;
292
+ console.error(`\n=== Indexing complete ===`);
293
+ console.error(`Files processed: ${processedFiles}`);
294
+ console.error(`Chunks created: ${totalChunks}`);
295
+ console.error(`Errors: ${errors.length}`);
296
+ console.error(`Duration: ${(indexDuration / 1000).toFixed(2)}s`);
297
+ // Run post-indexing hook to update project documentation
298
+ let docsGeneration;
299
+ if (shouldAutoUpdateDocs && this.projectKnowledgeService && changedFiles.length > 0) {
300
+ console.error(`\n=== Updating project documentation ===`);
301
+ try {
302
+ // Get all chunks for the project
303
+ const allChunks = await this.vectorStore.getAllChunks(projectId);
304
+ // Update docs incrementally based on changed files
305
+ docsGeneration = await this.projectKnowledgeService.updateDocuments(allChunks, changedFiles);
306
+ console.error(`Docs updated: ${docsGeneration.documentsUpdated.length}`);
307
+ console.error(`Docs generated: ${docsGeneration.documentsGenerated.length}`);
308
+ console.error(`Reasoning tokens: ${docsGeneration.totalReasoningTokens}`);
309
+ }
310
+ catch (error) {
311
+ console.error(`Warning: Failed to update project docs: ${error.message}`);
312
+ errors.push(`Project docs update failed: ${error.message}`);
236
313
  }
237
314
  }
238
- const duration = Date.now() - startTime;
239
- logger.info(`=== Indexing complete ===`);
240
- logger.info(`Files processed: ${processedFiles}`);
241
- logger.info(`Chunks created: ${totalChunks}`);
242
- logger.info(`Errors: ${errors.length}`);
243
- logger.info(`Duration: ${(duration / 1000).toFixed(2)}s`);
315
+ const totalDuration = Date.now() - startTime;
244
316
  return {
245
317
  filesProcessed: processedFiles,
318
+ changedFiles,
246
319
  chunksCreated: totalChunks,
247
320
  errors,
248
- duration,
321
+ duration: totalDuration,
322
+ docsGeneration,
249
323
  };
250
324
  }
251
325
  /**
252
326
  * Re-indexes a specific file by path
253
327
  */
254
- async reindexFile(filePath, rootPath, projectRoot) {
328
+ async reindexFile(filePath, rootPath, projectId) {
255
329
  try {
256
330
  // Scan the specific file
257
- const file = await scanSingleFile(filePath, rootPath, projectRoot);
331
+ const file = scanSingleFile(filePath, rootPath);
258
332
  if (!file) {
259
333
  return {
260
334
  success: false,
@@ -262,10 +336,12 @@ export class IndexManager {
262
336
  error: "File not found or not a code file",
263
337
  };
264
338
  }
339
+ // Derive project ID from root path if not provided
340
+ const resolvedProjectId = this.deriveProjectId(rootPath, projectId);
265
341
  // Initialize vector store
266
342
  await this.vectorStore.initialize();
267
343
  // Index the file
268
- const result = await this.indexFile(file, true);
344
+ const result = await this.indexFile(file, true, resolvedProjectId);
269
345
  if (result.error) {
270
346
  return {
271
347
  success: false,
@@ -318,6 +394,7 @@ export class IndexManager {
318
394
  const queryVector = await this.embeddingService.generateQueryEmbedding(query);
319
395
  // Search vector store
320
396
  const results = await this.vectorStore.search(queryVector, {
397
+ filterByProject: options.projectId,
321
398
  topK: options.topK || 10,
322
399
  minScore: options.minScore || 0.0,
323
400
  filterByFile: options.filterByFile,
@@ -325,11 +402,11 @@ export class IndexManager {
325
402
  });
326
403
  // Format results
327
404
  return results.map((result) => ({
328
- filePath: result.chunk.filePath,
405
+ filePath: result.chunk.file_path,
329
406
  content: result.chunk.content,
330
- startLine: result.chunk.startLine,
331
- endLine: result.chunk.endLine,
332
- chunkType: result.chunk.chunkType,
407
+ startLine: result.chunk.start_line,
408
+ endLine: result.chunk.end_line,
409
+ chunkType: result.chunk.chunk_type,
333
410
  name: result.chunk.name,
334
411
  language: result.chunk.language,
335
412
  score: result.score,
@@ -349,23 +426,23 @@ export class IndexManager {
349
426
  this.saveMetadata();
350
427
  // Clear embedding cache
351
428
  this.embeddingService.clearCache();
352
- logger.info("Index cleared");
429
+ console.error("Index cleared");
353
430
  }
354
431
  /**
355
432
  * Removes a file from the index
356
433
  */
357
434
  async removeFile(filePath) {
358
435
  await this.vectorStore.initialize();
359
- await this.vectorStore.deleteChunksByFile(filePath, this.projectId);
436
+ await this.vectorStore.deleteChunksByFile(filePath);
360
437
  delete this.metadata.files[filePath];
361
438
  this.saveMetadata();
362
- logger.info(`Removed ${filePath} from index`);
439
+ console.error(`Removed ${filePath} from index`);
363
440
  }
364
441
  }
365
442
  /**
366
443
  * Creates an index manager from environment variables
367
444
  */
368
- export function createIndexManager(embeddingService, vectorStore, workspaceRoot) {
445
+ export function createIndexManager(embeddingService, vectorStore) {
369
446
  const storagePath = process.env.MEMORYBANK_STORAGE_PATH || ".memorybank";
370
- return new IndexManager(embeddingService, vectorStore, storagePath, workspaceRoot);
447
+ return new IndexManager(embeddingService, vectorStore, storagePath);
371
448
  }