UNPKG

@z-test/memory-bank-mcp

Version:
274 lines 10.6 kB
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