Skip to main content
Glama

Memory Bank MCP

MemoryBankManager.ts23.5 kB
import path from 'path'; import { FileUtils } from '../utils/FileUtils.js'; import { coreTemplates } from './templates/index.js'; import { ProgressTracker } from './ProgressTracker.js'; import { ModeManager } from '../utils/ModeManager.js'; import { ExternalRulesLoader } from '../utils/ExternalRulesLoader.js'; import fs from 'fs'; import { MemoryBankStatus, ModeState } from '../types/index.js'; import { MigrationUtils } from '../utils/MigrationUtils.js'; import { logger } from '../utils/LogManager.js'; /** * Class responsible for managing Memory Bank operations * * This class handles all operations related to Memory Bank directories, * including initialization, file operations, and status tracking. */ export class MemoryBankManager { private memoryBankDir: string | null = null; private customPath: string | null = null; private progressTracker: ProgressTracker | null = null; private modeManager: ModeManager | null = null; private rulesLoader: ExternalRulesLoader | null = null; private projectPath: string | null = null; private userId: string | null = null; private folderName: string = 'memory-bank'; // Language is always set to English private language: string = 'en'; /** * Creates a new MemoryBankManager instance * * @param projectPath Optional project path to use instead of current directory * @param userId Optional GitHub profile URL for tracking changes * @param folderName Optional folder name for the Memory Bank (default: 'memory-bank') * @param debugMode Optional flag to enable debug mode */ constructor(projectPath?: string, userId?: string, folderName?: string, debugMode?: boolean) { // Ensure language is always English - this is a hard requirement // All Memory Bank content will be in English regardless of system locale or user settings this.language = 'en'; if (projectPath) { this.projectPath = projectPath; logger.debug('MemoryBankManager', `Initialized with project path: ${projectPath}`); } else { this.projectPath = process.cwd(); logger.debug('MemoryBankManager', `Initialized with current directory: ${this.projectPath}`); } this.userId = userId || "Unknown User"; logger.debug('MemoryBankManager', `Initialized with GitHub profile URL: ${this.userId}`); if (folderName) { this.folderName = folderName; logger.debug('MemoryBankManager', `Initialized with folder name: ${folderName}`); } else { logger.debug('MemoryBankManager', `Initialized with default folder name: ${this.folderName}`); } logger.info('MemoryBankManager', `Memory Bank language is set to English (${this.language}) - all content will be in English`); // Check for an existing memory-bank directory in the project path this.setCustomPath(this.projectPath).catch(error => { logger.error('MemoryBankManager', `Error checking for memory-bank directory: ${error}`); }); } /** * Gets the language used for the Memory Bank * * @returns The language code (always 'en' for English) */ getLanguage(): string { return this.language; } /** * Sets the language for the Memory Bank * * Note: This method is provided for API consistency, but the Memory Bank * will always use English (en) regardless of the language parameter. * This is a deliberate design decision to ensure consistency across all Memory Banks. * * @param language - Language code (ignored, always sets to 'en') */ setLanguage(language: string): void { // Always use English regardless of the parameter this.language = 'en'; console.warn('Memory Bank language is always set to English (en) regardless of the requested language. This is a hard requirement for consistency.'); } /** * Gets the project path * * @returns The project path */ getProjectPath(): string { return this.projectPath || process.cwd(); } /** * Finds a Memory Bank directory in the provided directory * * Combines the provided directory with the folder name to create the Memory Bank path. * * @param startDir - Starting directory for the search * @param customPath - Optional custom path (ignored in this implementation) * @returns Path to the Memory Bank directory or null if not found */ async findMemoryBankDir(startDir: string, customPath?: string): Promise<string | null> { // Combine the start directory with the folder name const mbDir = path.join(startDir, this.folderName); // Check if the directory exists and is a valid Memory Bank if (await FileUtils.fileExists(mbDir) && await FileUtils.isDirectory(mbDir)) { // Check if it's a valid Memory Bank or just a directory const files = await FileUtils.listFiles(mbDir); const mdFiles = files.filter(file => file.endsWith('.md')); if (mdFiles.length > 0) { return mbDir; } } // If directory doesn't exist or is not a valid Memory Bank, return null return null; } /** * Checks if a directory is a valid Memory Bank * * @param dirPath - Directory path to check * @returns True if it's a valid Memory Bank, false otherwise */ async isMemoryBank(dirPath: string): Promise<boolean> { try { if (!await FileUtils.isDirectory(dirPath)) { return false; } // Check if at least one of the core files exists const files = await FileUtils.listFiles(dirPath); // Support both camelCase and kebab-case during transition const coreFiles = [ // Kebab-case (new format) 'product-context.md', 'active-context.md', 'progress.md', 'decision-log.md', 'system-patterns.md', // CamelCase (old format) 'productContext.md', 'activeContext.md', 'progress.md', 'decisionLog.md', 'systemPatterns.md' ]; // Verificar cada arquivo individualmente for (const coreFile of coreFiles) { const filePath = path.join(dirPath, coreFile); if (await FileUtils.fileExists(filePath)) { return true; } } return false; } catch (error) { logger.error('MemoryBankManager', `Error checking if ${dirPath} is a Memory Bank: ${error}`); return false; } } /** * Validates if all required .clinerules files exist in the project root * * @param projectDir - Project directory to check * @returns Object with validation results */ async validateClinerules(projectDir: string): Promise<{ valid: boolean; missingFiles: string[]; existingFiles: string[]; }> { const requiredFiles = [ '.clinerules-architect', '.clinerules-ask', '.clinerules-code', '.clinerules-debug', '.clinerules-test' ]; const missingFiles: string[] = []; const existingFiles: string[] = []; for (const file of requiredFiles) { const filePath = path.join(projectDir, file); if (await FileUtils.fileExists(filePath)) { existingFiles.push(file); } else { missingFiles.push(file); } } return { valid: missingFiles.length === 0, missingFiles, existingFiles }; } /** * Initializes a Memory Bank in the specified directory * * Creates the necessary directory structure and files for a Memory Bank. * * @param dirPath - Directory path to initialize * @throws Error if initialization fails */ async initializeMemoryBank(dirPath: string): Promise<void> { try { // Combine the directory path with the folder name const memoryBankPath = path.join(dirPath, this.folderName); // Create the Memory Bank directory if it doesn't exist await FileUtils.ensureDirectory(memoryBankPath); // Create initial files in the Memory Bank directory const initialFiles = [ { path: path.join(memoryBankPath, 'product-context.md'), content: coreTemplates.find(t => t.name === 'product-context.md')?.content || '', }, { path: path.join(memoryBankPath, 'active-context.md'), content: coreTemplates.find(t => t.name === 'active-context.md')?.content || '', }, { path: path.join(memoryBankPath, 'progress.md'), content: coreTemplates.find(t => t.name === 'progress.md')?.content || '', }, { path: path.join(memoryBankPath, 'decision-log.md'), content: coreTemplates.find(t => t.name === 'decision-log.md')?.content || '', }, { path: path.join(memoryBankPath, 'system-patterns.md'), content: coreTemplates.find(t => t.name === 'system-patterns.md')?.content || '', }, ]; for (const file of initialFiles) { await FileUtils.writeFile(file.path, file.content); } // Set the Memory Bank directory this.memoryBankDir = memoryBankPath; // Initialize the progress tracker this.progressTracker = new ProgressTracker(memoryBankPath, this.userId || undefined); // Initialize the mode manager await this.initializeModeManager(); logger.debug('MemoryBankManager', `Memory Bank initialized at: ${memoryBankPath}`); } catch (error) { logger.error('MemoryBankManager', `Error initializing Memory Bank: ${error}`); throw new Error(`Failed to initialize Memory Bank: ${error}`); } } /** * Reads a file from the Memory Bank * * @param filename - Name of the file to read * @returns Content of the file * @throws Error if the Memory Bank directory is not set or the file doesn't exist */ async readFile(filename: string): Promise<string> { try { if (!this.memoryBankDir) { throw new Error('Memory Bank directory not set'); } const filePath = path.join(this.memoryBankDir, filename); if (!await FileUtils.fileExists(filePath)) { throw new Error(`File ${filename} not found in Memory Bank`); } return await FileUtils.readFile(filePath); } catch (error) { logger.error('MemoryBankManager', `Error reading file ${filename} from Memory Bank: ${error}`); throw new Error(`Error reading file ${filename} from Memory Bank: ${error instanceof Error ? error.message : String(error)}`); } } /** * Writes content to a file in the Memory Bank * * @param filename - Name of the file to write * @param content - Content to write * @throws Error if the Memory Bank directory is not set */ async writeFile(filename: string, content: string): Promise<void> { try { if (!this.memoryBankDir) { throw new Error('Memory Bank directory not set'); } const filePath = path.join(this.memoryBankDir, filename); await FileUtils.writeFile(filePath, content); // Track progress if ProgressTracker is available if (this.progressTracker) { try { await this.progressTracker.trackProgress('File Update', { description: `Updated ${filename}`, filename, }); } catch (progressError) { console.warn(`Failed to track progress for file update ${filename}:`, progressError); // Continue despite progress tracking error } } } catch (error) { logger.error('MemoryBankManager', `Error writing to file ${filename} in Memory Bank: ${error}`); throw new Error(`Error writing to file ${filename} in Memory Bank: ${error instanceof Error ? error.message : String(error)}`); } } /** * Lists files in the Memory Bank * * @returns List of files * @throws Error if the Memory Bank directory is not set */ async listFiles(): Promise<string[]> { try { if (!this.memoryBankDir) { throw new Error('Memory Bank directory not set'); } const files = await FileUtils.listFiles(this.memoryBankDir); return files.filter(file => file.endsWith('.md')); } catch (error) { console.error('Error listing files in Memory Bank:', error); throw new Error(`Error listing files in Memory Bank: ${error instanceof Error ? error.message : String(error)}`); } } /** * Gets the status of the Memory Bank * * @returns Status object with information about the Memory Bank * @throws Error if the Memory Bank directory is not set */ async getStatus(): Promise<MemoryBankStatus> { try { if (!this.memoryBankDir) { throw new Error('Memory Bank directory not set'); } const files = await this.listFiles(); const coreFiles = coreTemplates.map(template => template.name); const missingCoreFiles = coreFiles.filter(file => !files.includes(file)); // Get last update time let lastUpdated: Date | undefined; try { if (files.length > 0) { const stats = await Promise.all( files.map(async file => { try { const filePath = path.join(this.memoryBankDir!, file); return await FileUtils.getFileStats(filePath); } catch (statError) { console.warn(`Error getting stats for file ${file}:`, statError); // Return a default stat object with current time return { mtimeMs: Date.now(), } as fs.Stats; } }) ); const latestMtime = Math.max(...stats.map(stat => stat.mtimeMs)); lastUpdated = new Date(latestMtime); } } catch (statsError) { console.error('Error getting file stats:', statsError); // Continue without lastUpdated information } return { path: this.memoryBankDir, files, coreFilesPresent: coreFiles.filter(file => files.includes(file)), missingCoreFiles, isComplete: missingCoreFiles.length === 0, language: this.language, lastUpdated, }; } catch (error) { console.error('Error getting Memory Bank status:', error); throw new Error(`Error getting Memory Bank status: ${error instanceof Error ? error.message : String(error)}`); } } /** * Sets a custom path for the Memory Bank * * This will set the custom path and check if a valid Memory Bank * exists in that path. If it exists, it will set the Memory Bank directory. * * @param customPath - Custom path (optional) */ async setCustomPath(customPath?: string): Promise<void> { // Use the provided path or the project path const basePath = customPath || this.getProjectPath(); this.customPath = basePath; // Combine the base path with the folder name to create the Memory Bank path const memoryBankPath = path.join(basePath, this.folderName); if (await FileUtils.fileExists(memoryBankPath)) { if (await FileUtils.isDirectory(memoryBankPath)) { // Check if it's a valid Memory Bank if (await this.isMemoryBank(memoryBankPath)) { this.setMemoryBankDir(memoryBankPath); logger.debug('MemoryBankManager', `Found existing Memory Bank at: ${memoryBankPath}`); // Atualizar o status do Memory Bank await this.updateMemoryBankStatus(); } } } } /** * Gets the custom path for the Memory Bank * * @returns Custom path or null if not set */ getCustomPath(): string | null { return this.customPath; } /** * Gets the Memory Bank directory * * @returns Memory Bank directory or null if not set */ getMemoryBankDir(): string | null { return this.memoryBankDir; } /** * Sets the Memory Bank directory * * @param dir - Directory path */ setMemoryBankDir(dir: string): void { this.memoryBankDir = dir; // Initialize the progress tracker this.progressTracker = new ProgressTracker(dir, this.userId || undefined); // Initialize the mode manager this.initializeModeManager().catch(error => { console.error(`Error initializing mode manager: ${error}`); }); } /** * Gets the ProgressTracker * * @returns ProgressTracker or null if not available */ getProgressTracker(): ProgressTracker | null { return this.progressTracker; } /** * Creates a backup of the Memory Bank * * @param backupDir - Directory where the backup will be stored * @returns Path to the backup directory * @throws Error if the Memory Bank directory is not set or backup fails */ async createBackup(backupDir?: string): Promise<string> { if (!this.memoryBankDir) { throw new Error('Memory Bank directory not set'); } try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = backupDir ? path.join(backupDir, `memory-bank-backup-${timestamp}`) : path.join(path.dirname(this.memoryBankDir), `memory-bank-backup-${timestamp}`); await FileUtils.ensureDirectory(backupPath); const files = await this.listFiles(); for (const file of files) { const content = await this.readFile(file); await FileUtils.writeFile(path.join(backupPath, file), content); } logger.debug('MemoryBankManager', `Memory Bank backup created at ${backupPath}`); return backupPath; } catch (error) { logger.error('MemoryBankManager', `Error creating Memory Bank backup: ${error}`); throw new Error(`Failed to create Memory Bank backup: ${error}`); } } /** * Initializes the mode manager * * @param initialMode Initial mode to set (optional) * @returns Promise that resolves when initialization is complete */ async initializeModeManager(initialMode?: string): Promise<void> { if (this.modeManager) { return; // Already initialized } try { // Load external rules const projectRoot = this.getProjectPath(); this.rulesLoader = new ExternalRulesLoader(projectRoot); // Validate and create missing .clinerules files try { const validation = await this.rulesLoader.validateRequiredFiles(); if (!validation.valid) { console.warn(`Warning: Some .clinerules files could not be created: ${validation.missingFiles.join(', ')}`); console.warn('Continuing with mode manager initialization despite missing .clinerules files.'); } } catch (validationError) { console.warn('Error validating .clinerules files:', validationError); console.warn('Continuing with mode manager initialization despite validation errors.'); } try { await this.rulesLoader.detectAndLoadRules(); } catch (loadError) { console.warn('Error loading .clinerules files:', loadError); console.warn('Continuing with mode manager initialization despite loading errors.'); } // Create mode manager this.modeManager = new ModeManager(this.rulesLoader); try { await this.modeManager.initialize(initialMode); } catch (modeError) { console.warn('Error initializing mode manager:', modeError); console.warn('Mode manager may not be fully functional.'); } // Update Memory Bank status await this.updateMemoryBankStatus(); } catch (error) { logger.error('MemoryBankManager', `Error initializing mode manager: ${error}`); throw error; } } /** * Gets the mode manager * * @returns Mode manager or null if not initialized */ getModeManager(): ModeManager | null { return this.modeManager; } /** * Updates the Memory Bank status in the mode manager */ async updateMemoryBankStatus(): Promise<void> { if (this.modeManager) { let status: 'ACTIVE' | 'INACTIVE' = 'INACTIVE'; if (this.memoryBankDir) { // Check if the directory is a valid Memory Bank const isValid = await this.isMemoryBank(this.memoryBankDir); if (isValid) { status = 'ACTIVE'; } } this.modeManager.setMemoryBankStatus(status); } } /** * Gets the status prefix for responses * @returns Status prefix */ getStatusPrefix(): string { if (this.modeManager) { return this.modeManager.getStatusPrefix(); } return `[MEMORY BANK: ${this.memoryBankDir ? 'ACTIVE' : 'INACTIVE'}]`; } /** * Checks if a text matches the UMB trigger * @param text Text to be checked * @returns true if the text matches the UMB trigger, false otherwise */ checkUmbTrigger(text: string): boolean { if (this.modeManager) { return this.modeManager.checkUmbTrigger(text); } return false; } /** * Activates UMB mode * * @returns true if UMB mode was activated, false otherwise */ activateUmbMode(): boolean { if (this.modeManager) { return this.modeManager.activateUmb(); } return false; } /** * Deactivates UMB mode * * @returns true if UMB mode was deactivated, false otherwise */ async completeUmbMode(): Promise<boolean> { if (this.modeManager) { this.modeManager.deactivateUmb(); return true; } return false; } /** * Checks if UMB mode is active * @returns true if UMB mode is active, false otherwise */ isUmbModeActive(): boolean { if (this.modeManager) { return this.modeManager.isUmbModeActive(); } return false; } /** * Alterna para um modo específico * @param mode Nome do modo * @returns true se a mudança foi bem-sucedida, false caso contrário */ switchMode(mode: string): boolean { if (this.modeManager) { return this.modeManager.switchMode(mode); } return false; } /** * Checks if a text matches any mode trigger * @param text Text to be checked * @returns Array with modes corresponding to the triggers found */ checkModeTriggers(text: string): string[] { if (this.modeManager) { return this.modeManager.checkModeTriggers(text); } return []; } /** * Detects mode triggers in a message * @param message Message to check for triggers * @returns Array with modes corresponding to the triggers found */ detectModeTriggers(message: string): string[] { if (this.modeManager) { return this.modeManager.checkModeTriggers(message); } return []; } /** * Gets the folder name used for the Memory Bank * * @returns The folder name */ getFolderName(): string { return this.folderName; } /** * Migrates Memory Bank files from camelCase to kebab-case naming convention * * @returns Object with migration results * @throws Error if Memory Bank directory is not set */ async migrateFileNaming(): Promise<{ success: boolean; migrated: string[]; errors: string[]; }> { const memoryBankDir = this.getMemoryBankDir(); if (!memoryBankDir) { throw new Error('Memory Bank directory not set'); } const result = await MigrationUtils.migrateFileNamingConvention(memoryBankDir); return { success: result.success, migrated: result.migratedFiles, errors: result.errors }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/movibe/memory-bank-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server