@z-test/memory-bank-mcp
Version:
MCP Server for managing Memory Bank
274 lines • 10.6 kB
JavaScript
import { EventEmitter } from 'events';
import fs from 'fs-extra';
import { join } from 'path';
import { logger } from './LogManager.js';
import yaml from 'js-yaml';
import { clineruleTemplates } from './ClineruleTemplates.js';
/**
* Class responsible for loading and monitoring external .clinerules files
*/
export class ExternalRulesLoader extends EventEmitter {
/**
* Creates a new instance of the external rules loader
* @param projectDir Project directory (default: current directory)
*/
constructor(projectDir) {
super();
this.rules = new Map();
this.watchers = new Map();
this.projectDir = projectDir || process.cwd();
logger.debug('ExternalRulesLoader', `Initialized with project directory: ${this.projectDir}`);
}
/**
* Gets a writable directory for storing .clinerules files
* Uses only the specified project directory without fallbacks
* @returns A writable directory path
*/
async getWritableDirectory() {
// Use only the project directory
const targetDir = this.projectDir;
try {
await fs.access(targetDir, fs.constants.W_OK);
return targetDir;
}
catch (error) {
logger.error('ExternalRulesLoader', `Project directory ${targetDir} is not writable`);
throw new Error(`Project directory ${targetDir} is not writable`);
}
}
/**
* Validates that all required .clinerules files exist
* @returns Validation result with missing and existing files
*/
async validateRequiredFiles() {
const modes = ['architect', 'ask', 'code', 'debug', 'test'];
const missingFiles = [];
const existingFiles = [];
// Get a writable directory for .clinerules files
const targetDir = await this.getWritableDirectory();
// Check for files in both project directory and fallback directory
for (const mode of modes) {
const filename = `.clinerules-${mode}`;
const projectFilePath = join(this.projectDir, filename);
const fallbackFilePath = join(targetDir, filename);
if (await fs.pathExists(projectFilePath) || await fs.pathExists(fallbackFilePath)) {
existingFiles.push(filename);
}
else {
missingFiles.push(filename);
}
}
// If there are missing files, try to create them
if (missingFiles.length > 0) {
logger.warn('ExternalRulesLoader', `Missing .clinerules files: ${missingFiles.join(', ')}`);
const createdFiles = await this.createMissingClinerules(missingFiles);
// Update the lists
for (const file of createdFiles) {
const index = missingFiles.indexOf(file);
if (index !== -1) {
missingFiles.splice(index, 1);
existingFiles.push(file);
}
}
}
return {
valid: missingFiles.length === 0,
missingFiles,
existingFiles
};
}
/**
* Detects and loads all .clinerules files in the project directory
*/
async detectAndLoadRules() {
const modes = ['architect', 'ask', 'code', 'debug', 'test'];
// Validate required files and create missing ones
const validation = await this.validateRequiredFiles();
if (!validation.valid) {
logger.warn('ExternalRulesLoader', `Warning: Some .clinerules files could not be created: ${validation.missingFiles.join(', ')}`);
}
// Clear existing watchers
this.stopWatching();
// Clear existing rules
this.rules.clear();
// Get the fallback directory
const fallbackDir = await this.getWritableDirectory();
for (const mode of modes) {
const filename = `.clinerules-${mode}`;
const projectFilePath = join(this.projectDir, filename);
const fallbackFilePath = join(fallbackDir, filename);
try {
// First try to load from project directory
if (await fs.pathExists(projectFilePath)) {
const content = await fs.readFile(projectFilePath, 'utf8');
const rule = this.parseRuleContent(content);
if (rule && rule.mode === mode) {
this.rules.set(mode, rule);
logger.debug('ExternalRulesLoader', `Loaded ${filename} rules from project directory`);
// Set up watcher for this file
this.watchFile(projectFilePath);
}
else {
logger.warn('ExternalRulesLoader', `Invalid rule format in ${filename} (project directory)`);
}
}
// If not found in project directory, try fallback directory
else if (await fs.pathExists(fallbackFilePath)) {
const content = await fs.readFile(fallbackFilePath, 'utf8');
const rule = this.parseRuleContent(content);
if (rule && rule.mode === mode) {
this.rules.set(mode, rule);
logger.debug('ExternalRulesLoader', `Loaded ${filename} rules from fallback directory`);
// Set up watcher for this file
this.watchFile(fallbackFilePath);
}
else {
logger.warn('ExternalRulesLoader', `Invalid rule format in ${filename} (fallback directory)`);
}
}
}
catch (error) {
logger.warn('ExternalRulesLoader', `Error loading ${filename}: ${error}`);
}
}
return this.rules;
}
/**
* Parses the content of a rule file
* @param content File content
* @returns Parsed rule object or null if invalid
*/
parseRuleContent(content) {
try {
// First try to parse as JSON
const rule = JSON.parse(content);
// Basic validation
if (!rule.mode || !rule.instructions || !Array.isArray(rule.instructions.general)) {
return null;
}
return rule;
}
catch (jsonError) {
// If not valid JSON, try to parse as YAML
try {
const rule = yaml.load(content);
// Basic validation
if (!rule.mode || !rule.instructions || !Array.isArray(rule.instructions.general)) {
return null;
}
return rule;
}
catch (yamlError) {
console.error('Failed to parse rule content as JSON or YAML:', yamlError);
return null;
}
}
}
/**
* Sets up a watcher for a rule file
* @param filePath File path
*/
async watchFile(filePath) {
try {
const watcher = fs.watch(filePath, async (eventType) => {
try {
await this.loadRuleFile(filePath);
}
catch (error) {
logger.error('ExternalRulesLoader', `Error reloading ${filePath}: ${error}`);
}
});
this.watchers.set(filePath, watcher);
}
catch (error) {
logger.error('ExternalRulesLoader', `Error setting up file watcher for ${filePath}: ${error}`);
}
}
/**
* Stops watching all rule files
*/
stopWatching() {
for (const watcher of this.watchers.values()) {
watcher.close();
}
this.watchers.clear();
}
/**
* Gets the rules for a specific mode
* @param mode Mode name
* @returns Rules for the specified mode or null if not found
*/
getRulesForMode(mode) {
return this.rules.get(mode) || null;
}
/**
* Checks if a specific mode is available
* @param mode Mode name
* @returns true if the mode is available, false otherwise
*/
hasModeRules(mode) {
return this.rules.has(mode);
}
/**
* Gets all available modes
* @returns Array with the names of available modes
*/
getAvailableModes() {
return Array.from(this.rules.keys());
}
/**
* Cleans up all resources
*/
dispose() {
this.stopWatching();
this.removeAllListeners();
this.rules.clear();
}
/**
* Creates missing .clinerules files
* @param missingFiles Array of missing file names
* @returns Array of created file names
*/
async createMissingClinerules(missingFiles) {
const createdFiles = [];
// Get a writable directory for .clinerules files
const targetDir = await this.getWritableDirectory();
for (const filename of missingFiles) {
const mode = filename.replace('.clinerules-', '');
const template = clineruleTemplates[mode];
if (template) {
// Use only the path received via argument, without adding a folder
const filePath = join(targetDir, filename);
try {
await fs.writeFile(filePath, template);
createdFiles.push(filename);
logger.debug('ExternalRulesLoader', `Created ${filename} in ${targetDir}`);
}
catch (error) {
logger.error('ExternalRulesLoader', `Failed to create ${filename}: ${error}`);
}
}
else {
logger.warn('ExternalRulesLoader', `No template available for ${filename}`);
}
}
return createdFiles;
}
async loadRuleFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const rule = this.parseRuleContent(content);
if (rule) {
const mode = rule.mode;
this.rules.set(mode, rule);
this.emit('ruleChanged', mode, rule);
logger.debug('ExternalRulesLoader', `Updated ${join(this.projectDir, filePath).split('/').pop()} rules`);
}
}
catch (error) {
logger.error('ExternalRulesLoader', `Error loading rule file ${filePath}: ${error}`);
throw error;
}
}
}
//# sourceMappingURL=ExternalRulesLoader.js.map