@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
JavaScript
/**
* 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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