@z-test/memory-bank-mcp
Version:
MCP Server for managing Memory Bank
610 lines • 22.8 kB
JavaScript
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 { MigrationUtils } from '../utils/MigrationUtils.js';
import { logger } from '../utils/LogManager.js';
import { LocalStorageProvider } from './storage/LocalStorageProvider.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 {
/**
* 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
* @param storageProvider Optional storage provider to use instead of direct file system operations
*/
constructor(projectPath, userId, folderName, debugMode, storageProvider) {
this.memoryBankDir = null;
this.customPath = null;
this.progressTracker = null;
this.modeManager = null;
this.rulesLoader = null;
this.projectPath = null;
this.userId = null;
this.folderName = 'memory-bank';
// Language is always set to English
this.language = 'en';
// 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`);
this.storageProvider = storageProvider || new LocalStorageProvider();
// 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() {
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) {
// 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() {
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, _customPath) {
const mbDir = path.join(startDir, this.folderName);
if (await this.storageProvider.exists(mbDir)) {
const files = await this.storageProvider.listFiles(mbDir);
const mdFiles = files.filter(file => file.endsWith('.md'));
if (mdFiles.length > 0) {
return mbDir;
}
}
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) {
try {
if (!await this.storageProvider.exists(dirPath)) {
return false;
}
const files = await this.storageProvider.listFiles(dirPath);
const coreFiles = [
'product-context.md',
'active-context.md',
'progress.md',
'decision-log.md',
'system-patterns.md',
'productContext.md',
'activeContext.md',
'progress.md',
'decisionLog.md',
'systemPatterns.md'
];
for (const coreFile of coreFiles) {
if (files.includes(coreFile)) {
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) {
const requiredFiles = [
'.clinerules-architect',
'.clinerules-ask',
'.clinerules-code',
'.clinerules-debug',
'.clinerules-test'
];
const missingFiles = [];
const existingFiles = [];
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) {
try {
const memoryBankPath = path.join(dirPath, this.folderName);
await this.storageProvider.createDirectory(memoryBankPath);
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 this.storageProvider.writeFile(file.path, file.content);
}
this.memoryBankDir = memoryBankPath;
this.progressTracker = new ProgressTracker(memoryBankPath, this.userId || undefined);
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) {
try {
if (!this.memoryBankDir) {
throw new Error('Memory Bank directory not set');
}
const filePath = path.join(this.memoryBankDir, filename);
return await this.storageProvider.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, content) {
try {
if (!this.memoryBankDir) {
throw new Error('Memory Bank directory not set');
}
const filePath = path.join(this.memoryBankDir, filename);
await this.storageProvider.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() {
try {
if (!this.memoryBankDir) {
throw new Error('Memory Bank directory not set');
}
return await this.storageProvider.listFiles(this.memoryBankDir);
}
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() {
try {
if (!this.memoryBankDir) {
throw new Error('Memory Bank directory not set');
}
return await this.storageProvider.getStatus(this.memoryBankDir);
}
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) {
const basePath = _customPath || this.getProjectPath();
this.customPath = basePath;
const memoryBankPath = path.join(basePath, this.folderName);
if (await this.storageProvider.exists(memoryBankPath)) {
if (await this.isMemoryBank(memoryBankPath)) {
this.setMemoryBankDir(memoryBankPath);
logger.debug('MemoryBankManager', `Found existing Memory Bank at: ${memoryBankPath}`);
await this.updateMemoryBankStatus();
}
}
}
/**
* Gets the custom path for the Memory Bank
*
* @returns Custom path or null if not set
*/
getCustomPath() {
return this.customPath;
}
/**
* Gets the Memory Bank directory
*
* @returns Memory Bank directory or null if not set
*/
getMemoryBankDir() {
return this.memoryBankDir;
}
/**
* Sets the Memory Bank directory
*
* @param dir - Directory path
*/
setMemoryBankDir(dir) {
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() {
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) {
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 this.storageProvider.createBackup(this.memoryBankDir, backupPath);
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) {
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 MemoryBankConfig
const memoryBankConfig = {
files_to_read: [
'product-context.md',
'active-context.md',
'progress.md',
'decision-log.md',
'system-patterns.md'
],
files_to_update: [
'product-context.md',
'active-context.md',
'progress.md',
'decision-log.md',
'system-patterns.md'
],
options: {
auto_initialize: true,
create_missing_files: true,
backup_before_update: true
}
};
// Create mode manager
this.modeManager = new ModeManager(memoryBankConfig);
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() {
return this.modeManager;
}
/**
* Updates the Memory Bank status in the mode manager
*/
async updateMemoryBankStatus() {
if (this.modeManager) {
let status = '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() {
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) {
if (this.modeManager) {
return this.modeManager.checkUmbTrigger(text);
}
return false;
}
/**
* Activates UMB mode
*
* @returns true if UMB mode was activated, false otherwise
*/
activateUmbMode() {
if (this.modeManager) {
return this.modeManager.activateUmb();
}
return false;
}
/**
* Deactivates UMB mode
*
* @returns true if UMB mode was deactivated, false otherwise
*/
async completeUmbMode() {
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() {
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) {
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) {
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) {
if (this.modeManager) {
return this.modeManager.checkModeTriggers(message);
}
return [];
}
/**
* Gets the folder name used for the Memory Bank
*
* @returns The folder name
*/
getFolderName() {
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() {
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
};
}
}
//# sourceMappingURL=MemoryBankManager.js.map