UNPKG

@dollhousemcp/mcp-server

Version:

DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.

736 lines 108 kB
/** * Portfolio Index Manager - Maps element names to file paths * * Solves critical issues: * 1. submit_collection_content can't find elements by metadata name (e.g., "Safe Roundtrip Tester" -> "safe-roundtrip-tester.md") * 2. search_collection doesn't search local portfolio content * * Features: * - In-memory index mapping metadata.name → file path * - Keywords/tags → file paths mapping * - Element type → file paths mapping * - Fast O(1) lookups with Maps * - Lazy loading with 5-minute TTL cache * - Unicode normalization for security * - Error handling and logging */ import * as path from 'path'; import * as yaml from 'js-yaml'; import { logger } from '../utils/logger.js'; import { ElementType } from './types.js'; import { SecureYamlParser } from '../security/secureYamlParser.js'; import { UnicodeValidator } from '../security/validators/unicodeValidator.js'; import { SecurityMonitor } from '../security/securityMonitor.js'; import { ErrorHandler, ErrorCategory } from '../utils/ErrorHandler.js'; export class PortfolioIndexManager { indexConfigManager; index = null; lastBuilt = null; TTL_MS; portfolioManager; fileOperations; isBuilding = false; buildPromise = null; // Retry configuration for file operations MAX_RETRIES = 3; RETRY_DELAY_MS = 100; constructor(indexConfigManager, portfolioManager, fileOperations) { this.indexConfigManager = indexConfigManager; logger.debug('PortfolioIndexManager created'); this.TTL_MS = this.indexConfigManager.getConfig().index.ttlMinutes * 60 * 1000; this.portfolioManager = portfolioManager; this.fileOperations = fileOperations; } /** * Retry wrapper for file system operations * Handles transient file system errors with exponential backoff */ async retryFileOperation(operation, context, retries = this.MAX_RETRIES) { for (let attempt = 1; attempt <= retries; attempt++) { try { return await operation(); } catch (error) { const isLastAttempt = attempt === retries; const errorMessage = error instanceof Error ? error.message : String(error); // Check if error is retryable (transient file system errors) const isRetryable = errorMessage.includes('EBUSY') || errorMessage.includes('EAGAIN') || errorMessage.includes('ENOENT') || errorMessage.includes('ETIMEDOUT'); if (isLastAttempt || !isRetryable) { logger.warn(`File operation failed after ${attempt} attempts: ${context}`, { error: errorMessage, attempt, context }); return null; } // Exponential backoff const delay = this.RETRY_DELAY_MS * Math.pow(2, attempt - 1); logger.debug(`Retrying file operation: ${context}`, { attempt, nextDelay: delay, error: errorMessage }); await new Promise(resolve => setTimeout(resolve, delay)); } } return null; } /** * Get the current index, building it if necessary */ async getIndex() { // Check if we need to rebuild if (this.needsRebuild()) { await this.buildIndex(); } return this.index; } /** * Search the portfolio index by name with fuzzy matching */ async findByName(name, options = {}) { const index = await this.getIndex(); // Normalize input for security const normalizedName = UnicodeValidator.normalize(name); if (!normalizedName.isValid) { logger.warn('Invalid Unicode in search name', { issues: normalizedName.detectedIssues }); return null; } const safeName = normalizedName.normalizedContent; // Try exact match first (case insensitive) const exactMatch = index.byName.get(safeName.toLowerCase()); if (exactMatch) { logger.debug('Found exact name match', { name: safeName, filePath: exactMatch.filePath }); return exactMatch; } // Try filename match const filenameMatch = index.byFilename.get(safeName.toLowerCase()); if (filenameMatch) { logger.debug('Found filename match', { name: safeName, filePath: filenameMatch.filePath }); return filenameMatch; } // Try fuzzy matching if enabled if (options.fuzzyMatch !== false) { const fuzzyMatch = this.findFuzzyMatch(safeName, index, options); if (fuzzyMatch) { logger.debug('Found fuzzy match', { name: safeName, matchName: fuzzyMatch.metadata.name, filePath: fuzzyMatch.filePath }); return fuzzyMatch; } } logger.debug('No match found for name', { name: safeName }); return null; } /** * Search the portfolio with comprehensive text search */ async search(query, options = {}) { const index = await this.getIndex(); // Normalize query for security const normalizedQuery = UnicodeValidator.normalize(query); if (!normalizedQuery.isValid) { logger.warn('Invalid Unicode in search query', { issues: normalizedQuery.detectedIssues }); return []; } const safeQuery = normalizedQuery.normalizedContent.toLowerCase().trim(); const queryTokens = safeQuery.split(/\s+/).filter(token => token.length > 0); if (queryTokens.length === 0) { return []; } const results = []; const seenPaths = new Set(); const maxResults = options.maxResults || 20; // Helper to add unique results const addResult = (entry, matchType, score = 1) => { if (!seenPaths.has(entry.filePath) && results.length < maxResults) { // Filter by element type if specified if (options.elementType && entry.elementType !== options.elementType) { return; } seenPaths.add(entry.filePath); results.push({ entry, matchType, score }); } }; // 1. Search by name (highest priority) for (const [name, entry] of index.byName) { if (this.matchesQuery(name, queryTokens)) { addResult(entry, 'name', 3); } } // 2. Search by filename for (const [filename, entry] of index.byFilename) { if (this.matchesQuery(filename, queryTokens)) { addResult(entry, 'filename', 2.5); } } // 3. Search by keywords if (options.includeKeywords !== false) { for (const [keyword, entries] of index.byKeyword) { if (this.matchesQuery(keyword, queryTokens)) { for (const entry of entries) { addResult(entry, 'keyword', 2); } } } } // 4. Search by tags if (options.includeTags !== false) { for (const [tag, entries] of index.byTag) { if (this.matchesQuery(tag, queryTokens)) { for (const entry of entries) { addResult(entry, 'tag', 2); } } } } // 5. Search by triggers if (options.includeTriggers !== false) { for (const [trigger, entries] of index.byTrigger) { if (this.matchesQuery(trigger, queryTokens)) { for (const entry of entries) { addResult(entry, 'trigger', 1.8); } } } } // 6. Search by description if (options.includeDescriptions !== false) { for (const [, entry] of index.byName) { if (entry.metadata.description && this.matchesQuery(entry.metadata.description.toLowerCase(), queryTokens)) { addResult(entry, 'description', 1.5); } } } // Sort by score (descending) results.sort((a, b) => b.score - a.score); logger.debug('Portfolio search completed', { query: safeQuery, resultCount: results.length, totalIndexed: index.byName.size }); return results; } /** * Get all elements of a specific type */ async getElementsByType(elementType) { const index = await this.getIndex(); return index.byType.get(elementType) || []; } /** * Get statistics about the index */ async getStats() { const index = await this.getIndex(); const stats = { totalElements: index.byName.size, elementsByType: {}, lastBuilt: this.lastBuilt, isStale: this.needsRebuild() }; for (const elementType of Object.values(ElementType)) { stats.elementsByType[elementType] = (index.byType.get(elementType) || []).length; } return stats; } /** * Force rebuild the index */ async rebuildIndex() { this.index = null; this.lastBuilt = null; await this.buildIndex(); } /** * Check if the index needs rebuilding */ needsRebuild() { if (!this.index || !this.lastBuilt) { return true; } const age = Date.now() - this.lastBuilt.getTime(); return age > this.TTL_MS; } /** * Build the index by scanning all portfolio directories */ async buildIndex() { // Prevent concurrent builds if (this.isBuilding) { if (this.buildPromise) { await this.buildPromise; } return; } this.isBuilding = true; this.buildPromise = this.performBuild(); try { await this.buildPromise; } finally { this.isBuilding = false; this.buildPromise = null; } } /** * Perform the actual index building */ async performBuild() { const startTime = Date.now(); logger.debug('Building portfolio index...'); try { const portfolioManager = this.portfolioManager; // Initialize empty index const newIndex = { byName: new Map(), byFilename: new Map(), byType: new Map(), byKeyword: new Map(), byTag: new Map(), byTrigger: new Map() }; // Initialize type maps for (const elementType of Object.values(ElementType)) { newIndex.byType.set(elementType, []); } let totalFiles = 0; let processedFiles = 0; // Scan each element type for (const elementType of Object.values(ElementType)) { try { const elementDir = portfolioManager.getElementDir(elementType); // Check if directory exists const dirExists = await this.fileOperations.exists(elementDir); if (!dirExists) { logger.debug(`Element directory doesn't exist: ${elementDir}`); continue; } // FIX #1188: Special handling for memories - scan .yaml files in date folders if (elementType === ElementType.MEMORY) { // Memories are stored in date folders (YYYY-MM-DD) as .yaml files const entryNames = await this.fileOperations.listDirectory(elementDir); // Separate directories from files const directories = []; const rootYamlFiles = []; for (const entryName of entryNames) { const entryPath = path.join(elementDir, entryName); try { const entryStat = await this.fileOperations.stat(entryPath); if (entryStat.isDirectory()) { directories.push(entryName); } else if (entryName.endsWith('.yaml')) { rootYamlFiles.push(entryName); } } catch { // Skip entries we can't stat continue; } } for (const file of rootYamlFiles) { try { const filePath = path.join(elementDir, file); const entry = await this.createMemoryIndexEntry(filePath, elementType); if (entry) { this.addToIndex(newIndex, entry); processedFiles++; totalFiles++; } } catch (error) { logger.warn(`Failed to index root memory file`, { file, path: path.join(elementDir, file), location: 'root', error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error }); } } // Then process date folders const dateFolders = directories.filter(name => /^\d{4}-\d{2}-\d{2}$/.test(name)); for (const dateFolder of dateFolders) { const folderPath = path.join(elementDir, dateFolder); const folderEntryNames = await this.fileOperations.listDirectory(folderPath); // Separate directories from files in date folder const subDirs = []; const yamlFiles = []; for (const folderEntryName of folderEntryNames) { const folderEntryPath = path.join(folderPath, folderEntryName); try { const folderEntryStat = await this.fileOperations.stat(folderEntryPath); if (folderEntryStat.isDirectory()) { subDirs.push(folderEntryName); } else if (folderEntryName.endsWith('.yaml')) { yamlFiles.push(folderEntryName); } } catch { // Skip entries we can't stat continue; } } for (const file of yamlFiles) { try { const filePath = path.join(folderPath, file); const entry = await this.createMemoryIndexEntry(filePath, elementType); if (entry) { this.addToIndex(newIndex, entry); processedFiles++; totalFiles++; } } catch (error) { logger.warn(`Failed to index date folder memory file`, { file, path: path.join(folderPath, file), dateFolder, location: 'date-folder', error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error }); } } // FIX #1188: Process subdirectories for sharded memories // Large memories are stored as shards in named subdirectories for (const subDir of subDirs) { const subDirPath = path.join(folderPath, subDir); const shardFiles = await this.fileOperations.listDirectory(subDirPath); const shardYamlFiles = shardFiles.filter(file => file.endsWith('.yaml')); // For sharded memories, look for metadata.yaml or the main file // If not found, use the first shard as representative let metadataFile = shardYamlFiles.find(f => f === 'metadata.yaml') || shardYamlFiles.find(f => f === `${subDir}.yaml`) || shardYamlFiles[0]; if (metadataFile) { try { const filePath = path.join(subDirPath, metadataFile); const entry = await this.createMemoryIndexEntry(filePath, elementType); if (entry) { // Mark as sharded memory in metadata entry.metadata.keywords = entry.metadata.keywords || []; if (!entry.metadata.keywords.includes('sharded')) { entry.metadata.keywords.push('sharded'); } // Create properly typed sharded entry const shardedEntry = { ...entry, shardInfo: { shardCount: shardYamlFiles.length, shardDir: path.join(dateFolder, subDir), metadataFile: metadataFile } }; this.addToIndex(newIndex, shardedEntry); processedFiles++; totalFiles++; } } catch (error) { logger.warn(`Failed to index sharded memory`, { subDir, dateFolder, path: path.join(subDirPath, metadataFile), metadataFile, shardCount: shardYamlFiles.length, location: 'sharded-subdirectory', error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error, shardFiles: shardYamlFiles.slice(0, 5) // Log first 5 shard files for context }); } } } } } else { // Standard handling for other element types (.md files in root) const files = await this.fileOperations.listDirectory(elementDir); const mdFiles = files.filter(file => file.endsWith('.md')); totalFiles += mdFiles.length; for (const file of mdFiles) { try { const filePath = path.join(elementDir, file); const entry = await this.createIndexEntry(filePath, elementType); if (entry) { this.addToIndex(newIndex, entry); processedFiles++; } } catch (error) { logger.warn(`Failed to index file: ${file}`, { elementType, error: error instanceof Error ? error.message : String(error) }); } } } } catch (error) { logger.error(`Failed to scan element type: ${elementType}`, { error: error instanceof Error ? error.message : String(error) }); } } // Update instance state this.index = newIndex; this.lastBuilt = new Date(); const duration = Date.now() - startTime; logger.info('Portfolio index built successfully', { totalFiles, processedFiles, duration: `${duration}ms`, uniqueNames: newIndex.byName.size, uniqueKeywords: newIndex.byKeyword.size, uniqueTags: newIndex.byTag.size }); // Log security event for audit trail SecurityMonitor.logSecurityEvent({ type: 'PORTFOLIO_INITIALIZATION', severity: 'LOW', source: 'PortfolioIndexManager.performBuild', details: `Portfolio index rebuilt with ${processedFiles} elements in ${duration}ms` }); } catch (error) { ErrorHandler.logError('PortfolioIndexManager.performBuild', error); throw ErrorHandler.wrapError(error, 'Failed to build portfolio index', ErrorCategory.SYSTEM_ERROR); } } /** * Create an index entry from a file */ async createIndexEntry(filePath, elementType) { try { // Get file stats const stats = await this.fileOperations.stat(filePath); // Read file content const content = await this.fileOperations.readFile(filePath, { source: 'PortfolioIndexManager.createIndexEntry' }); // Parse frontmatter securely // SECURITY NOTE: Portfolio files are locally trusted content that users // have deliberately created or installed. Security validation should focus // on BEHAVIORAL analysis during import/installation, not superficial word // matching in descriptions. A malicious actor would never label their // exploit as "dangerous" - they'd call it "helpful utility". // Future: Add behavioral analysis on import, not during indexing. const parsed = SecureYamlParser.parse(content, { validateContent: false, // Don't scan for words in trusted local files validateFields: false // Portfolio files are pre-trusted by user choice }); // Extract base filename const filename = path.basename(filePath, '.md'); // Build metadata with defaults const metadata = { name: parsed.data.name || filename, description: parsed.data.description, version: parsed.data.version, author: parsed.data.author, tags: Array.isArray(parsed.data.tags) ? parsed.data.tags : [], keywords: Array.isArray(parsed.data.keywords) ? parsed.data.keywords : [], triggers: Array.isArray(parsed.data.triggers) ? parsed.data.triggers : [], category: parsed.data.category, created: parsed.data.created || parsed.data.created_date, updated: parsed.data.updated || parsed.data.updated_date, // Issue #749: Carry agent `activates` through to index builder for relationship extraction ...(parsed.data.activates && typeof parsed.data.activates === 'object' ? { activates: parsed.data.activates } : {}) }; const entry = { filePath, elementType, metadata, lastModified: stats.mtime, filename }; return entry; } catch (error) { logger.debug(`Failed to create index entry for: ${filePath}`, { error: error instanceof Error ? error.message : String(error) }); return null; } } /** * Create an index entry from a memory YAML file * FIX #1188: Special handling for memory files with different structure * FIX #1196: Use yaml.load for pure YAML files, not SecureYamlParser (which expects Markdown frontmatter) */ async createMemoryIndexEntry(filePath, elementType) { try { // Get file stats const stats = await this.fileOperations.stat(filePath); // Read file content const content = await this.fileOperations.readFile(filePath, { source: 'PortfolioIndexManager.createMemoryIndexEntry' }); // FIX #1196: Parse pure YAML using yaml.load() // Memory files are pure YAML without frontmatter markers, so we can't use SecureYamlParser // (which is designed for Markdown files with YAML frontmatter between --- markers) // Using FAILSAFE_SCHEMA for security (same as MemoryManager uses) // Security validation: Check content size before parsing if (content.length > 1048576) { // 1MB limit logger.warn(`Large memory file detected, skipping: ${filePath}`); return null; } const rawParsed = yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA }); // Type safety: Ensure parsed result is a valid object if (!rawParsed || typeof rawParsed !== 'object' || Array.isArray(rawParsed)) { logger.warn(`Invalid YAML structure in memory file: ${filePath}`); return null; } const parsed = rawParsed; // Extract base filename const filename = path.basename(filePath, '.yaml'); // Memory files can have metadata at top level OR nested under 'metadata' key // FIX #1196: Merge both levels, preferring nested metadata block over top-level // This handles mixed structures where some fields are top-level and others are nested const metadataSource = parsed.metadata ? { ...parsed, ...parsed.metadata } // Merge top-level with nested, nested wins : parsed; // No nested metadata, use top-level only // Build metadata with memory-specific defaults const metadata = { name: metadataSource.name || filename.replaceAll('-', ' '), description: metadataSource.description || 'Memory element', version: metadataSource.version || '1.0.0', author: metadataSource.author, tags: Array.isArray(metadataSource.tags) ? metadataSource.tags : [], keywords: Array.isArray(metadataSource.keywords) ? metadataSource.keywords : [], triggers: Array.isArray(metadataSource.triggers) ? metadataSource.triggers : [], category: metadataSource.category, created: metadataSource.created || metadataSource.created_date, updated: metadataSource.updated || metadataSource.updated_date || metadataSource.modified }; const entry = { filePath, elementType, metadata, lastModified: stats.mtime, filename }; return entry; } catch (error) { logger.debug(`Failed to create memory index entry for: ${filePath}`, { error: error instanceof Error ? error.message : String(error) }); return null; } } /** * Add entry to all relevant index maps */ addToIndex(index, entry) { // Normalize keys for case-insensitive lookup const normalizedName = entry.metadata.name.toLowerCase(); const normalizedFilename = entry.filename.toLowerCase(); // Add to name map index.byName.set(normalizedName, entry); // Add to filename map index.byFilename.set(normalizedFilename, entry); // Add to type map const typeEntries = index.byType.get(entry.elementType) || []; typeEntries.push(entry); index.byType.set(entry.elementType, typeEntries); // Add keywords for (const keyword of entry.metadata.keywords || []) { const normalizedKeyword = keyword.toLowerCase(); const keywordEntries = index.byKeyword.get(normalizedKeyword) || []; keywordEntries.push(entry); index.byKeyword.set(normalizedKeyword, keywordEntries); } // Add tags for (const tag of entry.metadata.tags || []) { const normalizedTag = tag.toLowerCase(); const tagEntries = index.byTag.get(normalizedTag) || []; tagEntries.push(entry); index.byTag.set(normalizedTag, tagEntries); } // Add triggers for (const trigger of entry.metadata.triggers || []) { const normalizedTrigger = trigger.toLowerCase(); const triggerEntries = index.byTrigger.get(normalizedTrigger) || []; triggerEntries.push(entry); index.byTrigger.set(normalizedTrigger, triggerEntries); } } /** * Find fuzzy matches for a name */ findFuzzyMatch(searchName, index, options) { const search = searchName.toLowerCase(); let bestMatch = null; let bestScore = 0; // Search names with partial matching for (const [name, entry] of index.byName) { if (options.elementType && entry.elementType !== options.elementType) { continue; } const score = this.calculateSimilarity(search, name); if (score > bestScore && score > 0.3) { // Minimum similarity threshold bestScore = score; bestMatch = entry; } } // Also check filenames for (const [filename, entry] of index.byFilename) { if (options.elementType && entry.elementType !== options.elementType) { continue; } const score = this.calculateSimilarity(search, filename); if (score > bestScore && score > 0.3) { bestScore = score; bestMatch = entry; } } return bestMatch; } /** * Calculate similarity between two strings */ calculateSimilarity(a, b) { // Simple similarity based on substring containment and length if (a === b) return 1.0; if (a.includes(b) || b.includes(a)) return 0.8; // Check for word overlap const wordsA = a.split(/\s+/); const wordsB = b.split(/\s+/); const commonWords = wordsA.filter(word => wordsB.includes(word)); if (commonWords.length > 0) { return commonWords.length / Math.max(wordsA.length, wordsB.length); } return 0; } /** * Check if any query tokens match the text */ matchesQuery(text, queryTokens) { return queryTokens.some(token => text.includes(token)); } /** * Dispose internal state to release resources (used during shutdown/tests). */ dispose() { this.index = null; this.lastBuilt = null; this.isBuilding = false; this.buildPromise = null; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUG9ydGZvbGlvSW5kZXhNYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3BvcnRmb2xpby9Qb3J0Zm9saW9JbmRleE1hbmFnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztHQWVHO0FBRUgsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFDN0IsT0FBTyxLQUFLLElBQUksTUFBTSxTQUFTLENBQUM7QUFDaEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFFekMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbkUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sNENBQTRDLENBQUM7QUFDOUUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ2pFLE9BQU8sRUFBRSxZQUFZLEVBQUUsYUFBYSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUE4RHZFLE1BQU0sT0FBTyxxQkFBcUI7SUFjYjtJQWJYLEtBQUssR0FBMEIsSUFBSSxDQUFDO0lBQ3BDLFNBQVMsR0FBZ0IsSUFBSSxDQUFDO0lBQ3JCLE1BQU0sQ0FBUztJQUNmLGdCQUFnQixDQUFtQjtJQUNuQyxjQUFjLENBQXlCO0lBQ2hELFVBQVUsR0FBRyxLQUFLLENBQUM7SUFDbkIsWUFBWSxHQUF5QixJQUFJLENBQUM7SUFFbEQsMENBQTBDO0lBQ3pCLFdBQVcsR0FBRyxDQUFDLENBQUM7SUFDaEIsY0FBYyxHQUFHLEdBQUcsQ0FBQztJQUV0QyxZQUNtQixrQkFBc0MsRUFDdkQsZ0JBQWtDLEVBQ2xDLGNBQXNDO1FBRnJCLHVCQUFrQixHQUFsQixrQkFBa0IsQ0FBb0I7UUFJdkQsTUFBTSxDQUFDLEtBQUssQ0FBQywrQkFBK0IsQ0FBQyxDQUFDO1FBQzlDLElBQUksQ0FBQyxNQUFN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