UNPKG

@memory-bank/mcp

Version:

Memory-enabled Co-Pilot (MCP) server for managing project documentation and context

341 lines 12.8 kB
import path from 'node:path'; import { DocumentId } from '../../domain/entities/DocumentId.js'; import { DocumentPath } from '../../domain/entities/DocumentPath.js'; import { JsonDocument } from '../../domain/entities/JsonDocument.js'; import { InfrastructureError, InfrastructureErrorCodes, } from '../../shared/errors/InfrastructureError.js'; import { TAG_INDEX_VERSION as INDEX_SCHEMA_VERSION, } from '@memory-bank/schemas'; /** * Implementation of the document index service * Provides efficient lookup capabilities for documents using in-memory indices * with file system persistence */ export class IndexService { fileSystemService; rootPath; indices = new Map(); /** * Create a new IndexService * @param fileSystemService File system service for file operations * @param rootPath Root path for storing index files */ constructor(fileSystemService, rootPath) { this.fileSystemService = fileSystemService; this.rootPath = rootPath; } /** * Initialize the index for a branch * @param branchInfo Branch information */ async initializeIndex(branchInfo) { try { await this.loadIndex(branchInfo); } catch (error) { const newIndex = { schema: INDEX_SCHEMA_VERSION, lastUpdated: new Date(), branchName: branchInfo.name, idIndex: {}, pathIndex: {}, typeIndex: {}, tagIndex: {}, }; this.indices.set(branchInfo.name, newIndex); await this.saveIndex(branchInfo); } } /** * Build the index from a collection of documents * @param branchInfo Branch information * @param documents Documents to index */ async buildIndex(branchInfo, documents) { const newIndex = { schema: INDEX_SCHEMA_VERSION, lastUpdated: new Date(), branchName: branchInfo.name, idIndex: {}, pathIndex: {}, typeIndex: {}, tagIndex: {}, }; for (const document of documents) { this.addDocumentToIndex(newIndex, document); } this.indices.set(branchInfo.name, newIndex); await this.saveIndex(branchInfo); } /** * Add a document to the index * @param branchInfo Branch information * @param document Document to add */ async addToIndex(branchInfo, document) { const index = await this.getOrCreateIndex(branchInfo); this.addDocumentToIndex(index, document); index.lastUpdated = new Date(); await this.saveIndex(branchInfo); } /** * Remove a document from the index * @param branchInfo Branch information * @param document Document or document ID or path to remove */ async removeFromIndex(branchInfo, document) { const index = await this.getOrCreateIndex(branchInfo); let documentId; if (document instanceof JsonDocument) { documentId = document.id.value; } else if (document instanceof DocumentId) { documentId = document.value; } else if (document instanceof DocumentPath) { documentId = index.pathIndex[document.value]; if (!documentId) { return; } } else { throw new InfrastructureError(InfrastructureErrorCodes.INVALID_ARGUMENT, 'Invalid document reference type'); } const docRef = index.idIndex[documentId]; if (!docRef) { return; } delete index.pathIndex[docRef.path]; const typeArray = index.typeIndex[docRef.documentType]; if (typeArray) { index.typeIndex[docRef.documentType] = typeArray.filter((id) => id !== documentId); if (index.typeIndex[docRef.documentType].length === 0) { delete index.typeIndex[docRef.documentType]; } } for (const [tag, documentIds] of Object.entries(index.tagIndex)) { const ids = documentIds; index.tagIndex[tag] = ids.filter((id) => id !== documentId); if (index.tagIndex[tag].length === 0) { delete index.tagIndex[tag]; } } delete index.idIndex[documentId]; index.lastUpdated = new Date(); await this.saveIndex(branchInfo); } /** * Find document reference by ID * @param branchInfo Branch information * @param id Document ID * @returns Document reference if found, null otherwise */ async findById(branchInfo, id) { const index = await this.getOrCreateIndex(branchInfo); const docRef = index.idIndex[id.value]; if (!docRef) return null; return this.convertToDocumentReference(docRef); } /** * Convert internal document reference to the schema DocumentReference type * @param internal Internal document reference * @returns Schema-compatible document reference */ convertToDocumentReference(internal) { // Exclude documentType as it's not in the schema's DocumentReference return { id: internal.id, path: internal.path, title: internal.title, lastModified: internal.lastModified }; } /** * Find document reference by path * @param branchInfo Branch information * @param path Document path * @returns Document reference if found, null otherwise */ async findByPath(branchInfo, path) { const index = await this.getOrCreateIndex(branchInfo); const documentId = index.pathIndex[path.value]; if (!documentId) { return null; } const docRef = index.idIndex[documentId]; if (!docRef) { return null; } return this.convertToDocumentReference(docRef); } /** * Find document references by tags * @param branchInfo Branch information * @param tags Tags to search for * @param matchAll If true, documents must have all tags; if false, any tag is sufficient * @returns Array of matching document references */ // Changed parameters to object literal type async findByTags(params) { const { branchInfo, tags, matchAll = false } = params; const index = await this.getOrCreateIndex(branchInfo); if (tags.length === 0) { return []; } let matchingIds; if (matchAll) { const firstTag = tags[0].value; const firstTagDocs = index.tagIndex[firstTag] || []; matchingIds = new Set(firstTagDocs); for (let i = 1; i < tags.length; i++) { const tagDocs = new Set(index.tagIndex[tags[i].value] || []); matchingIds = new Set([...matchingIds].filter((id) => tagDocs.has(id))); } } else { matchingIds = new Set(); for (const tag of tags) { const tagDocs = index.tagIndex[tag.value] || []; for (const docId of tagDocs) { matchingIds.add(docId); } } } return Array.from(matchingIds) .map((id) => index.idIndex[id]) .filter((ref) => ref !== undefined) .map((ref) => this.convertToDocumentReference(ref)); } /** * Find document references by document type * @param branchInfo Branch information * @param documentType Document type to search for * @returns Array of matching document references */ async findByType(branchInfo, documentType) { const index = await this.getOrCreateIndex(branchInfo); const documentIds = index.typeIndex[documentType] || []; return documentIds .map((id) => index.idIndex[id]) .filter((ref) => ref !== undefined) .map((ref) => this.convertToDocumentReference(ref)); } /** * List all document references * @param branchInfo Branch information * @returns Array of all document references */ async listAll(branchInfo) { const index = await this.getOrCreateIndex(branchInfo); return Object.values(index.idIndex).map(ref => this.convertToDocumentReference(ref)); } /** * Save the index to persistent storage * @param branchInfo Branch information */ async saveIndex(branchInfo) { const index = this.indices.get(branchInfo.name); if (!index) { throw new InfrastructureError(InfrastructureErrorCodes.FILE_NOT_FOUND, `Index for branch ${branchInfo.name} not found`); } const indexFilePath = this.getIndexFilePath(branchInfo); const indexDir = path.dirname(indexFilePath); await this.fileSystemService.createDirectory(indexDir); try { await this.fileSystemService.writeFile(indexFilePath, JSON.stringify(index, null, 2)); } catch (error) { throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to save index: ${error.message}`, { cause: error }); } } /** * Load the index from persistent storage * @param branchInfo Branch information */ async loadIndex(branchInfo) { const indexFilePath = this.getIndexFilePath(branchInfo); try { const exists = await this.fileSystemService.fileExists(indexFilePath); if (!exists) { throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Index file not found: ${indexFilePath}`); } const indexJson = await this.fileSystemService.readFile(indexFilePath); const index = JSON.parse(indexJson); this.indices.set(branchInfo.name, index); } catch (error) { throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to load index: ${error.message}`, { cause: error }); } } /** * Get the index file path for a branch * @param branchInfo Branch information * @returns Absolute file path to the index file */ getIndexFilePath(branchInfo) { const normalizedBranchName = branchInfo.name.replace(/\//g, '-'); return path.join(this.rootPath, normalizedBranchName, '_index.json'); } /** * Get an existing index or create a new one if it doesn't exist * @param branchInfo Branch information * @returns Document index */ async getOrCreateIndex(branchInfo) { let index = this.indices.get(branchInfo.name); if (!index) { try { await this.loadIndex(branchInfo); index = this.indices.get(branchInfo.name); } catch (error) { index = { schema: INDEX_SCHEMA_VERSION, lastUpdated: new Date(), branchName: branchInfo.name, idIndex: {}, pathIndex: {}, typeIndex: {}, tagIndex: {}, }; this.indices.set(branchInfo.name, index); } } if (!index) { throw new InfrastructureError(InfrastructureErrorCodes.FILE_NOT_FOUND, `Index for branch ${branchInfo.name} not found`); } return index; } /** * Add a document to an index * @param index Document index to update * @param document Document to add */ addDocumentToIndex(index, document) { const documentId = document.id.value; const docRef = { id: documentId, path: document.path.value, documentType: document.documentType, title: document.title, lastModified: document.lastModified, }; index.idIndex[documentId] = docRef; index.pathIndex[document.path.value] = documentId; if (!index.typeIndex[document.documentType]) { index.typeIndex[document.documentType] = []; } if (!index.typeIndex[document.documentType].includes(documentId)) { index.typeIndex[document.documentType].push(documentId); } for (const tag of document.tags) { const tagValue = tag.value; if (!index.tagIndex[tagValue]) { index.tagIndex[tagValue] = []; } if (!index.tagIndex[tagValue].includes(documentId)) { index.tagIndex[tagValue].push(documentId); } } } } //# sourceMappingURL=IndexService.js.map