UNPKG

@memory-bank/mcp

Version:

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

538 lines 24.8 kB
import path from 'node:path'; import { DomainError } from '../../../shared/errors/DomainError.js'; import { InfrastructureError, InfrastructureErrors } from '../../../shared/errors/InfrastructureError.js'; import { logger } from '../../../shared/utils/logger.js'; import { FileSystemMemoryBankRepositoryBase } from './FileSystemMemoryBankRepositoryBase.js'; import { DocumentOperations } from './DocumentOperations.js'; import { TagOperations } from './TagOperations.js'; import { PathOperations } from './PathOperations.js'; import { BulkOperations } from './BulkOperations.js'; /** * File system implementation of global memory bank repository * Using component-based approach with responsibility segregation */ export class FileSystemGlobalMemoryBankRepository extends FileSystemMemoryBankRepositoryBase { configProvider; componentLogger = logger.withContext({ component: 'FileSystemGlobalMemoryBankRepository' }); documentOps; tagOps; pathOps; bulkOps; globalMemoryPath; language = 'en'; /** * Constructor * @param fileSystemService File system service * @param configProvider Configuration provider */ constructor(fileSystemService, configProvider) { super(fileSystemService, configProvider); this.configProvider = configProvider; this.documentOps = new DocumentOperations('', // Path set in setup() fileSystemService, configProvider); this.tagOps = new TagOperations('', // Path set in setup() fileSystemService, configProvider); this.pathOps = new PathOperations('', // Path set in setup() fileSystemService, configProvider); this.bulkOps = new BulkOperations('', // Path set in setup() fileSystemService, configProvider); } /** * Ensure default structure exists */ async ensureDefaultStructure() { const operation = 'ensureDefaultStructure'; this.componentLogger.debug(`Starting ${operation}`); try { for (const [relativePath, content] of Object.entries(this.defaultStructure)) { const fullPath = path.join(this.globalMemoryPath, relativePath); const dirPath = path.dirname(fullPath); await this.pathOps.createDirectory(dirPath); const fileExists = await this.fileExists(fullPath); if (!fileExists) { this.componentLogger.debug(`Default file does not exist, creating...`, { operation, relativePath, fullPath }); await this.writeFile(fullPath, content); this.componentLogger.info(`Created default file`, { operation, relativePath }); } else { this.componentLogger.debug(`Default file already exists`, { operation, relativePath, fullPath }); } } this.componentLogger.info(`${operation} completed successfully`); } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); // Wrap error using factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation }); } } /** * Get document from global memory bank * @param path Document path * @returns Promise resolving to document if found, null otherwise */ async getDocument(path) { const operation = 'getGlobalDocument'; this.componentLogger.debug(`Starting ${operation}`, { documentPath: path.value }); try { await this.setup(); const document = await this.documentOps.getDocument(path); this.componentLogger.debug(`${operation} completed`, { documentPath: path.value, found: !!document }); return document; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { documentPath: path.value, error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory, include cause in details throw InfrastructureErrors.fileReadError(`Failed to get global document: ${path.value}`, { operation, documentPath: path.value, cause: error instanceof Error ? error : undefined }); } } /** * Save document to global memory bank * @param document Document to save * @returns Promise resolving when done */ async saveDocument(document) { const operation = 'saveGlobalDocument'; const documentPathValue = document.path.value; this.componentLogger.debug(`Starting ${operation}`, { documentPath: documentPathValue }); try { await this.setup(); await this.documentOps.saveDocument(document); this.componentLogger.debug(`Document saved via documentOps`, { documentPath: documentPathValue }); const isIndexFile = documentPathValue === 'tags/index.md' || documentPathValue === 'tags/index.json' || documentPathValue === '_global_index.json'; if (!isIndexFile) { this.componentLogger.debug(`Document is not an index file, refreshing tag index`, { documentPath: documentPathValue }); await this.refreshTagIndex(); } else { this.componentLogger.debug(`Document is an index file, skipping tag index refresh`, { documentPath: documentPathValue }); } this.componentLogger.info(`${operation} completed successfully`, { documentPath: documentPathValue }); } catch (error) { this.componentLogger.error(`Error during ${operation}`, { documentPath: documentPathValue, error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory, include cause in details throw InfrastructureErrors.fileWriteError(`Failed to save global document: ${documentPathValue}`, { operation, documentPath: documentPathValue, cause: error instanceof Error ? error : undefined }); } } /** * Delete document from global memory bank * @param path Document path * @returns Promise resolving to boolean indicating success */ async deleteDocument(path) { const operation = 'deleteGlobalDocument'; const documentPathValue = path.value; this.componentLogger.debug(`Starting ${operation}`, { documentPath: documentPathValue }); try { await this.setup(); const document = await this.getDocument(path); if (!document) { this.componentLogger.warn(`Document not found, cannot delete`, { operation, documentPath: documentPathValue }); return false; } this.componentLogger.debug(`Document found, proceeding with deletion`, { documentPath: documentPathValue }); const result = await this.documentOps.deleteDocument(path); this.componentLogger.debug(`Deletion result from documentOps`, { documentPath: documentPathValue, deleted: result }); const isIndexFile = documentPathValue === 'tags/index.md' || documentPathValue === 'tags/index.json' || documentPathValue === '_global_index.json'; if (result && !isIndexFile) { this.componentLogger.debug(`Document deleted and is not an index file, refreshing tag index`, { documentPath: documentPathValue }); await this.refreshTagIndex(); } else if (result) { this.componentLogger.debug(`Document deleted but is an index file, skipping tag index refresh`, { documentPath: documentPathValue }); } this.componentLogger.info(`${operation} completed`, { documentPath: documentPathValue, deleted: result }); return result; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { documentPath: documentPathValue, error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation} for document ${documentPathValue}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation, documentPath: documentPathValue }); } } /** * List all documents in global memory bank * @returns Promise resolving to array of document paths */ async listDocuments() { const operation = 'listGlobalDocuments'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); const paths = await this.documentOps.listDocuments(); this.componentLogger.info(`${operation} completed successfully`, { count: paths.length }); return paths; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation }); } } /** * Find documents by tags in global memory bank * @param tags Tags to search for * @returns Promise resolving to array of matching documents */ async findDocumentsByTags(tags) { const operation = 'findGlobalDocumentsByTags'; const tagValues = tags.map(t => t.value); this.componentLogger.debug(`Starting ${operation}`, { tags: tagValues }); try { await this.setup(); const paths = await this.findDocumentPathsByTagsUsingIndex(tags, false); this.componentLogger.debug(`Found paths using index (or fallback)`, { count: paths.length }); const documents = []; for (const docPath of paths) { const doc = await this.getDocument(docPath); if (doc) { documents.push(doc); } else { this.componentLogger.warn(`Document path found by index but document not retrieved`, { operation, documentPath: docPath.value }); } } this.componentLogger.info(`${operation} completed successfully`, { tags: tagValues, foundCount: documents.length }); return documents; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { tags: tagValues, error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation, tags: tagValues }); } } /** * Refresh all tag indexes (internal helper) */ async refreshTagIndex() { const operation = 'refreshGlobalTagIndex'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); const tagIndex = await this.tagOps.generateGlobalTagIndex(); this.componentLogger.debug(`Generated global tag index`); await this.saveTagIndex(tagIndex); this.componentLogger.info(`${operation} completed successfully`); // Deprecated legacy index update removed (dd-deprecate-legacy-index) } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); } } /** * Validate global memory bank structure * @returns Promise resolving to boolean indicating if structure is valid */ async validateStructure() { const operation = 'validateGlobalStructure'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); this.componentLogger.debug(`Setup complete, proceeding with validation`); const dirExists = await this.pathOps.directoryExists(this.globalMemoryPath); if (!dirExists) { this.componentLogger.warn(`Global memory bank directory does not exist`, { operation, path: this.globalMemoryPath }); return false; } this.componentLogger.debug(`Global directory exists`, { path: this.globalMemoryPath }); const tagsDir = path.join(this.globalMemoryPath, 'tags'); const tagsDirExists = await this.pathOps.directoryExists(tagsDir); if (!tagsDirExists) { this.componentLogger.warn(`Tags directory does not exist`, { operation, path: tagsDir }); return false; } this.componentLogger.debug(`Tags directory exists`, { path: tagsDir }); for (const filePath of Object.keys(this.defaultStructure)) { const fullPath = path.join(this.globalMemoryPath, filePath); const fileExists = await this.fileExists(fullPath); if (!fileExists) { this.componentLogger.warn(`Required default file missing`, { operation, filePath, fullPath }); return false; } this.componentLogger.debug(`Required default file exists`, { filePath, fullPath }); } this.componentLogger.info(`${operation} completed successfully, structure is valid`); return true; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); return false; } } /** * Save tag index for global memory bank * @param tagIndex Tag index to save * @returns Promise resolving when done */ async saveTagIndex(tagIndex) { const operation = 'saveGlobalTagIndex'; this.componentLogger.debug(`Starting ${operation}`, { indexSchema: tagIndex.schema, indexType: tagIndex.metadata.indexType }); try { await this.setup(); await this.tagOps.saveGlobalTagIndex(tagIndex); this.componentLogger.info(`${operation} completed successfully`); } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileWriteError('Global Tag Index', { operation, reason: 'Failed during tag index saving', cause: error instanceof Error ? error : undefined }); } } /** * Get tag index for global memory bank * @returns Promise resolving to tag index if found, null otherwise */ async getTagIndex() { const operation = 'getGlobalTagIndex'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); const result = await this.tagOps.getGlobalTagIndex(); this.componentLogger.info(`${operation} completed successfully`, { found: !!result }); return result; } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileReadError('Global Tag Index', { operation, reason: 'Failed during tag index retrieval', cause: error instanceof Error ? error : undefined }); } } /** * Update tags index in global memory bank (legacy method) * @returns Promise resolving when done * @deprecated Use saveTagIndex instead */ async updateTagsIndex() { const operation = 'updateTagsIndex (legacy)'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); await this.refreshTagIndex(); this.componentLogger.info(`${operation} completed successfully by calling refreshTagIndex`); } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation }); } } /** * Find documents by tags in global memory bank using index * @param tags Tags to search for * @param matchAll If true, documents must have all tags (AND); if false, any tag (OR) * @returns Promise resolving to array of document paths */ async findDocumentPathsByTagsUsingIndex(tags, matchAll = false) { const operation = 'findGlobalDocumentPathsByTagsUsingIndex'; const tagValues = tags.map(t => t.value); this.componentLogger.debug(`Starting ${operation}`, { tags: tagValues, matchAll }); try { await this.setup(); const paths = await this.tagOps.findDocumentPathsByTagsUsingIndex(tags, undefined, matchAll); this.componentLogger.info(`${operation} completed successfully using index`, { tags: tagValues, matchAll, foundCount: paths.length }); return paths; } catch (error) { this.componentLogger.warn(`Tag index search failed, falling back to findDocumentsByTags`, { operation, tags: tagValues, matchAll, error }); try { // Call findDocumentPathsByTags instead and use the result directly const paths = await this.tagOps.findDocumentPathsByTags(tags, matchAll); // Removed undefined argument this.componentLogger.info(`${operation} completed successfully via fallback`, { tags: tagValues, matchAll, foundCount: paths.length }); } catch (fallbackError) { this.componentLogger.error(`Error during ${operation} (including fallback)`, { tags: tagValues, matchAll, indexError: error, fallbackError }); if (fallbackError instanceof DomainError || fallbackError instanceof InfrastructureError) { throw fallbackError; } // Wrap unknown errors from fallback using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation} (including fallback): ${fallbackError.message}`, { cause: fallbackError instanceof Error ? fallbackError : undefined, operation, tags: tagValues, matchAll }); } } // Add return statement for the outer catch block in case of index search failure but fallback also fails unexpectedly // Although the inner catch re-throws, this satisfies the compiler. return []; } /** * Setup repository with configuration * This should be called before using the repository */ async setup() { const operation = 'setupGlobalRepository'; if (!this.globalMemoryPath) { this.componentLogger.debug(`Performing initial setup for ${operation}`); this.globalMemoryPath = this.configProvider.getGlobalMemoryPath(); this.componentLogger.debug(`Global memory path set`, { operation, path: this.globalMemoryPath }); this.documentOps.basePath = this.globalMemoryPath; this.tagOps.basePath = this.globalMemoryPath; this.pathOps.basePath = this.globalMemoryPath; this.bulkOps.basePath = this.globalMemoryPath; this.componentLogger.debug(`Operation component paths updated`, { operation }); } const newLanguage = this.configProvider.getLanguage(); if (this.language !== newLanguage) { this.componentLogger.debug(`Language updated`, { operation, oldLanguage: this.language, newLanguage }); this.language = newLanguage; } } /** * Get default templates based on current language */ get defaultStructure() { switch (this.language) { case 'ja': return this.getJapaneseTemplates(); case 'zh': return this.getChineseTemplates(); default: return this.getEnglishTemplates(); } } /** * Get English templates for default structure * @returns Record of file paths to template content */ getEnglishTemplates() { return { 'tags/index.json': `{ "schema": "memory_document_v2", "metadata": { "id": "tags-index", "title": "Tags Index", "documentType": "generic", "path": "tags/index.json", "tags": ["index", "meta"], "lastModified": "${new Date().toISOString()}", "createdAt": "${new Date().toISOString()}", "version": 1 }, "content": { "sections": [ { "title": "Tags List", "content": "[List and description of tags]" } ] } }` }; } /** * Get Japanese templates for default structure * @returns Record of file paths to template content */ getJapaneseTemplates() { return { 'tags/index.json': `{ "schema": "memory_document_v2", "metadata": { "id": "tags-index", "title": "タグインデックス", "documentType": "generic", "path": "tags/index.json", "tags": ["index", "meta"], "lastModified": "${new Date().toISOString()}", "createdAt": "${new Date().toISOString()}", "version": 1 }, "content": { "sections": [ { "title": "タグ一覧", "content": "[タグの一覧と説明]" } ] } }` }; } /** * Get Chinese templates for default structure * @returns Record of file paths to template content */ getChineseTemplates() { return { 'tags/index.json': `{ "schema": "memory_document_v2", "metadata": { "id": "tags-index", "title": "标签索引", "documentType": "generic", "path": "tags/index.json", "tags": ["index", "meta"], "lastModified": "${new Date().toISOString()}", "createdAt": "${new Date().toISOString()}", "version": 1 }, "content": { "sections": [ { "title": "标签列表", "content": "[标签列表和描述]" } ] } }` }; } /** * Initialize global memory bank * @returns Promise resolving when initialization is complete */ async initialize() { const operation = 'initializeGlobalMemoryBank'; this.componentLogger.debug(`Starting ${operation}`); try { await this.setup(); await this.pathOps.createDirectory(this.globalMemoryPath); await this.pathOps.createDirectory(path.join(this.globalMemoryPath, 'tags')); this.componentLogger.debug(`Base directories ensured`, { operation }); await this.ensureDefaultStructure(); try { await this.refreshTagIndex(); } catch (tagIndexError) { this.componentLogger.error(`Failed during initial tag index refresh, but continuing initialization`, { operation, error: tagIndexError }); this.componentLogger.warn(`Some documents may have invalid tags. Consider running a repair script.`, { operation }); } this.componentLogger.info(`${operation} completed successfully`); } catch (error) { this.componentLogger.error(`Error during ${operation}`, { error }); if (error instanceof DomainError || error instanceof InfrastructureError) { throw error; } // Wrap unknown errors using the factory throw InfrastructureErrors.fileSystemError(`Failed to ${operation}: ${error.message}`, { cause: error instanceof Error ? error : undefined, operation }); } } } //# sourceMappingURL=FileSystemGlobalMemoryBankRepository.js.map