UNPKG

@memory-bank/mcp

Version:

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

345 lines 12.9 kB
/** * File-based implementation of the Template Repository * * Stores and retrieves templates from JSON files. * Each template is stored in its own file with all language variations. */ import path from 'path'; import fs from 'fs/promises'; import { logger } from '../../shared/utils/logger.js'; // Import logger import { Template } from '../../domain/templates/Template.js'; import { Section } from '../../domain/templates/Section.js'; /** * File-based implementation of Template Repository */ export class FileTemplateRepository { i18nProvider; componentLogger = logger.withContext({ component: 'FileTemplateRepository' }); // Add logger instance basePath; templateCache; cacheDirty; /** * Constructor * * @param basePath Base path for storing template files * @param i18nProvider Optional I18n provider for translations */ constructor(basePath, i18nProvider) { this.i18nProvider = i18nProvider; this.basePath = basePath; this.templateCache = new Map(); this.cacheDirty = true; } /** * Initialize the repository * * @returns Promise resolving when initialization is complete */ async initialize() { try { // Directory creation is handled externally or assumed to exist. // await fs.mkdir(this.basePath, { recursive: true }); // Removed directory creation // Load all templates into cache await this.loadAllTemplates(); } catch (error) { throw new Error(`Failed to initialize Template repository: ${error.message}`); } } /** * Gets a template by ID * * @param id Template ID * @returns Promise resolving to Template or null if not found */ async getTemplate(id) { if (this.cacheDirty) { await this.loadAllTemplates(); } return this.templateCache.get(id) || null; } /** * Gets a template by ID as a JSON object with resolved translations * * @param id Template ID * @param language Language to get template for * @param _variables Optional variables for template substitution (currently not used for JSON) * @returns Promise resolving to the template content as a JSON object * @throws Error if template not found or cannot be parsed as JSON */ async getTemplateAsJsonObject(id, language, _variables // Keep variables for interface consistency ) { const template = await this.getTemplate(id); if (!template) { throw new Error(`Template not found: ${id}`); } // Construct the JSON object with resolved translations const jsonObject = { id: template.id, type: template.type, name: template.getName(language), // Get translated name sections: template.sections.map(section => ({ id: section.id, title: section.getTitle(language), // Get translated title content: section.getContent(language), // Get translated content isOptional: section.isOptional, })), }; return jsonObject; } /** * Gets all templates of a specific type * * @param type Template type * @returns Promise resolving to array of Templates */ async getTemplatesByType(type) { if (this.cacheDirty) { await this.loadAllTemplates(); } const templates = []; for (const template of this.templateCache.values()) { if (template.type === type) { templates.push(template); } } return templates; } /** * Saves a template (creates or updates) * * @param template Template to save * @returns Promise resolving to boolean indicating success */ async saveTemplate(template) { try { // Update cache this.templateCache.set(template.id, template); // Save to file await this.saveTemplateToFile(template); return true; } catch (error) { this.componentLogger.error(`Failed to save template: ${template.id}`, { error }); // Use componentLogger return false; } } /** * Checks if a template exists * * @param id Template ID * @returns Promise resolving to boolean indicating if template exists */ async templateExists(id) { if (this.cacheDirty) { await this.loadAllTemplates(); } return this.templateCache.has(id); } /** * Gets IDs of all available templates * * @returns Promise resolving to array of template IDs */ async getAllTemplateIds() { if (this.cacheDirty) { await this.loadAllTemplates(); } return Array.from(this.templateCache.keys()); } /** * Gets all available template types * * @returns Promise resolving to array of template types */ async getAllTemplateTypes() { if (this.cacheDirty) { await this.loadAllTemplates(); } const types = new Set(); for (const template of this.templateCache.values()) { types.add(template.type); } return Array.from(types); } /** * Loads all templates into cache * * @private */ async loadAllTemplates() { try { // Clear the cache this.templateCache.clear(); // Check if directory exists try { await fs.access(this.basePath); } catch (error) { // Directory doesn't exist, create it await fs.mkdir(this.basePath, { recursive: true }); // No files to load this.cacheDirty = false; return; } // Get all template files const files = await fs.readdir(this.basePath); const templateFiles = files.filter(file => file.endsWith('.json')); // Load each file for (const file of templateFiles) { const id = path.basename(file, '.json'); await this.loadTemplate(id); } // Cache is now clean this.cacheDirty = false; } catch (error) { this.componentLogger.error('Failed to load templates', { error }); // Use componentLogger throw new Error(`Failed to load templates: ${error.message}`); } } /** * Loads a template by ID * * @param id Template ID * @private */ async loadTemplate(id) { try { const filePath = path.join(this.basePath, `${id}.json`); // Check if file exists try { await fs.access(filePath); } catch (error) { // File doesn't exist return; } // Read and parse the file const content = await fs.readFile(filePath, 'utf-8'); const data = JSON.parse(content); // Check if this is a template_v1 format if (data.schema === 'template_v1' && data.metadata && data.content) { // Handle template_v1 format return await this.loadTemplateV1(data, id); } // Validate data for legacy format if (data.id !== id) { throw new Error(`Template ID mismatch in file ${filePath}: expected ${id}, got ${data.id}`); } // Ensure sections exist if (!data.sections || !Array.isArray(data.sections)) { throw new Error(`Template ${id} has no sections`); } // Convert to domain model const sections = data.sections.map(sectionData => { return new Section(sectionData.id, sectionData.titles, sectionData.contents, sectionData.isOptional); }); const template = new Template(data.id, data.type, data.names || {}, // Ensure names is not undefined sections); // Add to cache this.templateCache.set(id, template); } catch (error) { this.componentLogger.error(`Failed to load template: ${id}`, { error }); // Use componentLogger throw new Error(`Failed to load template ${id}: ${error.message}`); } } /** * Loads a template in v1 format * * @param data Template data in v1 format * @param id Template ID * @private */ async loadTemplateV1(data, id) { try { if (!data.metadata || !data.content || !data.content.sections) { throw new Error(`Invalid template_v1 format for template ${id}`); } // Create translation maps for each section const sections = data.content.sections.map(sectionData => { // Create title map with translations if available const titleMap = {}; // Add English translation if (this.i18nProvider && sectionData.titleKey) { // Use i18nProvider to get actual translation titleMap.en = this.i18nProvider.translate(sectionData.titleKey, 'en'); titleMap.ja = this.i18nProvider.translate(sectionData.titleKey, 'ja'); titleMap.zh = this.i18nProvider.translate(sectionData.titleKey, 'zh'); } else { // Fallback to using key as value titleMap.en = sectionData.titleKey; } // Create content map with translations if available const contentMap = {}; // Add English translation if (this.i18nProvider && sectionData.contentKey) { // Use i18nProvider to get actual translation contentMap.en = this.i18nProvider.translate(sectionData.contentKey, 'en'); contentMap.ja = this.i18nProvider.translate(sectionData.contentKey, 'ja'); contentMap.zh = this.i18nProvider.translate(sectionData.contentKey, 'zh'); } else { // Fallback to using key as value contentMap.en = sectionData.contentKey; } return new Section(sectionData.id, titleMap, contentMap, sectionData.isOptional); }); // Create name map with translations if available const nameMap = {}; if (this.i18nProvider && data.metadata.titleKey) { // Use i18nProvider to get actual translation nameMap.en = this.i18nProvider.translate(data.metadata.titleKey, 'en'); nameMap.ja = this.i18nProvider.translate(data.metadata.titleKey, 'ja'); nameMap.zh = this.i18nProvider.translate(data.metadata.titleKey, 'zh'); } else { // Fallback to using key as value nameMap.en = data.metadata.titleKey || id; } const template = new Template(id, data.metadata.type, nameMap, sections); // Add to cache this.templateCache.set(id, template); } catch (error) { this.componentLogger.error(`Failed to load template_v1: ${id}`, { error }); // Use componentLogger throw new Error(`Failed to load template_v1 ${id}: ${error.message}`); } } /** * Saves a template to file * * @param template Template to save * @private */ async saveTemplateToFile(template) { try { // Convert to file format const sections = template.sections.map(section => { return { id: section.id, titles: section.titleMap, contents: section.contentMap, isOptional: section.isOptional }; }); const data = { id: template.id, type: template.type, names: template.nameMap, sections, lastModified: new Date().toISOString() }; // Write to file const filePath = path.join(this.basePath, `${template.id}.json`); await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); } catch (error) { this.componentLogger.error(`Failed to save template to file: ${template.id}`, { error }); // Use componentLogger throw new Error(`Failed to save template ${template.id}: ${error.message}`); } } } //# sourceMappingURL=FileTemplateRepository.js.map