@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.
868 lines • 118 kB
JavaScript
/**
* PortfolioSyncManager - Handles synchronization between local and GitHub portfolios
*
* Features:
* - Download elements from GitHub portfolio
* - Upload elements with consent
* - Version comparison and diff viewing
* - Privacy-first with explicit permissions
* - Conflict resolution strategies
* - Bulk operations with configuration checks
*/
import * as path from 'path';
import { createHash } from 'crypto';
import { logger } from '../utils/logger.js';
import { ContentValidator } from '../security/contentValidator.js';
import { UnicodeValidator } from '../security/validators/unicodeValidator.js';
import { SecureYamlParser } from '../security/secureYamlParser.js';
import { ElementType } from './types.js';
import { PortfolioElementAdapter } from '../tools/portfolio/PortfolioElementAdapter.js';
export class PortfolioSyncManager {
configManager;
portfolioManager;
repoManager;
indexer;
fileOperations;
tokenManager;
constructor(dependencies) {
this.configManager = dependencies.configManager;
this.portfolioManager = dependencies.portfolioManager;
this.repoManager = dependencies.portfolioRepoManager;
this.indexer = dependencies.indexer;
this.fileOperations = dependencies.fileOperations;
this.tokenManager = dependencies.tokenManager;
}
/**
* Main handler for sync operations
*/
async handleSyncOperation(params) {
try {
// Check if sync is enabled in config
const config = this.configManager.getConfig();
if (!config.sync.enabled && params.operation !== 'list-remote') {
return {
success: false,
message: 'Sync is disabled. Enable it with: dollhouse_config --action update --setting sync.enabled --value true'
};
}
// Check bulk permissions
if (params.bulk) {
const bulkAllowed = this.isBulkOperationAllowed(params.operation, config);
if (!bulkAllowed.allowed) {
return {
success: false,
message: bulkAllowed.message
};
}
}
// Handle operations
switch (params.operation) {
case 'list-remote':
return await this.listRemoteElements(params.element_type);
case 'download':
if (params.bulk) {
return await this.bulkDownload(params.element_type, params.confirm);
}
else if (params.element_name) {
return await this.downloadElement(params.element_name, params.element_type, params.version, params.force);
}
else {
return {
success: false,
message: 'Element name required for individual download'
};
}
case 'upload':
if (params.bulk) {
return await this.bulkUpload(params.element_type, params.confirm);
}
else if (params.element_name) {
return await this.uploadElement(params.element_name, params.element_type, params.confirm);
}
else {
return {
success: false,
message: 'Element name required for individual upload'
};
}
case 'compare':
if (params.element_name && params.element_type) {
return await this.compareVersions(params.element_name, params.element_type, params.show_diff);
}
else {
return {
success: false,
message: 'Element name and type required for comparison'
};
}
default:
return {
success: false,
message: `Unknown operation: ${params.operation}`
};
}
}
catch (error) {
logger.error('Sync operation failed', {
operation: params.operation,
error: error instanceof Error ? error.message : String(error)
});
return {
success: false,
message: `Sync operation failed: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Check if bulk operation is allowed
*/
isBulkOperationAllowed(operation, config) {
if (operation === 'download' && !config.sync.bulk.download_enabled) {
return {
allowed: false,
message: 'Bulk download is disabled. Enable with: dollhouse_config --action update --setting sync.bulk.download_enabled --value true'
};
}
if (operation === 'upload' && !config.sync.bulk.upload_enabled) {
return {
allowed: false,
message: 'Bulk upload is disabled. Enable with: dollhouse_config --action update --setting sync.bulk.upload_enabled --value true'
};
}
return { allowed: true, message: '' };
}
/**
* List elements available in GitHub portfolio
*/
async listRemoteElements(filterType) {
try {
// Get GitHub token
const token = await this.tokenManager.getGitHubTokenAsync();
if (!token) {
return {
success: false,
message: 'GitHub authentication required. Use setup_github_auth first.'
};
}
this.repoManager.setToken(token);
// Get index of GitHub portfolio
const index = await this.indexer.getIndex();
if (!index || index.totalElements === 0) {
return {
success: true,
message: 'No elements found in GitHub portfolio',
elements: []
};
}
// Format elements for display
const elements = [];
for (const [type, entries] of index.elements) {
// Skip if filtering by type and this isn't the requested type
if (filterType && type !== filterType) {
continue;
}
for (const entry of entries) {
elements.push({
name: entry.name,
type: type,
remoteVersion: entry.version,
status: 'unchanged',
action: 'download'
});
}
}
return {
success: true,
message: `Found ${elements.length} elements in GitHub portfolio`,
elements
};
}
catch (error) {
return {
success: false,
message: `Failed to list remote elements: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Download a specific element from GitHub
*/
async downloadElement(elementName, elementType, version, force) {
try {
const config = this.configManager.getConfig();
// Validate element name
const validation = UnicodeValidator.normalize(elementName);
if (!validation.isValid) {
return {
success: false,
message: `Invalid element name: ${validation.detectedIssues?.[0] || 'unknown error'}`
};
}
// Get token and set it
const token = await this.tokenManager.getGitHubTokenAsync();
if (!token) {
return {
success: false,
message: 'GitHub authentication required'
};
}
this.repoManager.setToken(token);
// Get GitHub index
const index = await this.indexer.getIndex();
// Find the element - first try exact match, then fuzzy match
const entries = index.elements.get(elementType) || [];
let entry = entries.find(e => e.name === elementName);
// If exact match not found, try fuzzy matching
if (!entry) {
// Try case-insensitive exact match first
entry = entries.find(e => e.name.toLowerCase() === elementName.toLowerCase());
// If still not found, try fuzzy matching
if (!entry) {
const fuzzyMatch = this.findFuzzyMatch(elementName, entries);
if (fuzzyMatch) {
logger.info(`Fuzzy match found: '${elementName}' matched to '${fuzzyMatch.name}'`);
entry = fuzzyMatch;
}
}
}
if (!entry) {
// Generate helpful suggestions
const suggestions = this.getSuggestions(elementName, entries);
const suggestionText = suggestions.length > 0
? `\n\nDid you mean one of these?\n${suggestions.map(s => ` • ${s.name}`).join('\n')}`
: '';
return {
success: false,
message: `Element '${elementName}' (${elementType}) not found in GitHub portfolio${suggestionText}`
};
}
// Check for local conflicts
const localPath = this.portfolioManager.getElementPath(elementType, `${elementName}.md`);
let hasLocalVersion = false;
let localContent = null;
try {
localContent = await this.fileOperations.readFile(localPath, { source: 'PortfolioSyncManager.downloadElement' });
hasLocalVersion = true;
}
catch {
// No local version exists
}
// Download the element
const response = await fetch(entry.downloadUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3.raw'
}
});
if (!response.ok) {
throw new Error(`Failed to download: ${response.statusText}`);
}
const remoteContent = await response.text();
// Validate content security
const validationResult = ContentValidator.validateAndSanitize(remoteContent);
if (!validationResult.isValid && validationResult.severity === 'critical') {
return {
success: false,
message: `Security issue detected in remote content: ${validationResult.detectedPatterns?.join(', ')}`
};
}
// Check if content is different
if (hasLocalVersion && localContent) {
const localHash = createHash('sha256').update(localContent).digest('hex');
const remoteHash = createHash('sha256').update(remoteContent).digest('hex');
if (localHash === remoteHash) {
return {
success: true,
message: `Element '${elementName}' is already up to date`
};
}
// Show confirmation for overwrite unless force flag is set
if (config.sync.individual.require_confirmation && !force) {
const diff = await this.generateDiff(localContent, remoteContent);
const conflictInfo = await this.buildConflictInfo(elementName, elementType, localPath, localContent, entry);
logger.warn('Sync conflict detected', {
element: elementName,
type: elementType,
conflict: conflictInfo
});
return {
success: false,
message: `Local version exists. Please confirm download will overwrite:\n\n${diff}\n\nTo proceed, use --force flag`,
data: { requiresConfirmation: true },
conflicts: [conflictInfo]
};
}
}
// Save the element
await this.fileOperations.createDirectory(path.dirname(localPath));
await this.fileOperations.writeFile(localPath, remoteContent, { source: 'PortfolioSyncManager.downloadElement' });
logger.info('Element downloaded from GitHub', {
element: elementName,
type: elementType,
version: entry.version
});
return {
success: true,
message: `Successfully downloaded '${elementName}' (${elementType}) from GitHub portfolio`
};
}
catch (error) {
return {
success: false,
message: `Failed to download element: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Upload a specific element to GitHub
*/
async uploadElement(elementName, elementType, confirm) {
try {
const config = this.configManager.getConfig();
// Check for local element
const localPath = this.portfolioManager.getElementPath(elementType, `${elementName}.md`);
let content;
try {
content = await this.fileOperations.readFile(localPath, { source: 'PortfolioSyncManager.uploadElement' });
}
catch {
return {
success: false,
message: `Element '${elementName}' (${elementType}) not found locally`
};
}
// Check privacy metadata
const parsed = SecureYamlParser.parse(content, {
maxYamlSize: 64 * 1024,
validateContent: false,
validateFields: false
});
if (parsed.data?.privacy?.local_only === true) {
return {
success: false,
message: `Element '${elementName}' is marked as local-only and cannot be uploaded`
};
}
// Validate content security
const validationResult = ContentValidator.validateAndSanitize(content);
if (!validationResult.isValid && validationResult.severity === 'critical') {
return {
success: false,
message: `Security issue detected: ${validationResult.detectedPatterns?.join(', ')}`
};
}
// Scan for sensitive content if configured
if (config.sync.privacy.scan_for_secrets) {
logger.debug('Scanning for secrets before upload');
// Implement actual secret scanning
const secretPatterns = [
/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi,
/secret\s*[:=]\s*['"][^'"]+['"]/gi,
/password\s*[:=]\s*['"][^'"]+['"]/gi,
/token\s*[:=]\s*['"][^'"]+['"]/gi,
/private[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi
];
for (const pattern of secretPatterns) {
if (pattern.test(content)) {
return {
success: false,
message: `Potential secret detected in content. Please review and remove sensitive information before uploading.`
};
}
}
}
// Get confirmation if required (unless already confirmed)
if (config.sync.individual.require_confirmation && !confirm) {
return {
success: false,
message: `Please confirm upload of '${elementName}' (${elementType}) to GitHub.\n\nContent preview:\n${content.substring(0, 500)}...\n\nTo proceed, use --confirm flag`,
data: { requiresConfirmation: true }
};
}
// Get token and validate
const token = await this.tokenManager.getGitHubTokenAsync();
if (!token) {
return {
success: false,
message: 'GitHub authentication required'
};
}
// Create a PortfolioElement for the adapter (fixes Issue #913)
// Using PortfolioElementAdapter instead of incomplete IElement implementation
const portfolioElement = {
type: elementType,
metadata: {
name: elementName,
description: parsed.data?.description || '',
author: parsed.data?.author || 'unknown',
created: parsed.data?.created || new Date().toISOString(),
updated: new Date().toISOString(),
version: parsed.data?.version || '1.0.0',
tags: parsed.data?.tags || []
},
content: content
};
// Use PortfolioElementAdapter to properly implement IElement interface
const adapter = new PortfolioElementAdapter(portfolioElement);
// Use PortfolioRepoManager to upload
this.repoManager.setToken(token);
// DEBUG: Log upload attempt
logger.debug('[BULK_SYNC_DEBUG] Upload element attempt', {
elementName,
elementType,
hasToken: !!token,
tokenPrefix: token ? token.substring(0, 10) + '...' : 'none',
adapterHasMetadata: !!(adapter && adapter.metadata),
timestamp: new Date().toISOString()
});
try {
const url = await this.repoManager.saveElement(adapter, true); // consent is true since we've already checked
logger.info('Element uploaded to GitHub', {
element: elementName,
type: elementType,
url
});
return {
success: true,
message: `Successfully uploaded '${elementName}' (${elementType}) to GitHub portfolio`,
data: { url }
};
}
catch (uploadError) {
// Handle specific errors
if (uploadError instanceof Error && uploadError.message.includes('repository does not exist')) {
return {
success: false,
message: `GitHub portfolio repository not found. Please initialize it first using init_portfolio tool.`
};
}
throw uploadError;
}
}
catch (error) {
return {
success: false,
message: `Failed to upload element: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Compare local and remote versions
*/
async compareVersions(elementName, elementType, showDiff) {
try {
// Get local version
const localPath = this.portfolioManager.getElementPath(elementType, `${elementName}.md`);
let localContent = null;
let localVersion = null;
try {
localContent = await this.fileOperations.readFile(localPath, { source: 'PortfolioSyncManager.compareVersions' });
const parsed = SecureYamlParser.parse(localContent, {
maxYamlSize: 64 * 1024,
validateContent: false,
validateFields: false
});
localVersion = {
version: parsed.data?.version || '1.0.0',
timestamp: new Date(parsed.data?.updated || parsed.data?.created || Date.now()),
author: parsed.data?.author || 'unknown',
hash: createHash('sha256').update(localContent).digest('hex'),
size: Buffer.byteLength(localContent),
source: 'local'
};
}
catch {
// No local version
}
// Get remote version
const token = await this.tokenManager.getGitHubTokenAsync();
if (!token) {
return {
success: false,
message: 'GitHub authentication required'
};
}
const index = await this.indexer.getIndex();
const entries = index.elements.get(elementType) || [];
const entry = entries.find(e => e.name === elementName);
let remoteVersion = null;
let remoteContent = null;
if (entry) {
const response = await fetch(entry.downloadUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3.raw'
}
});
if (response.ok) {
remoteContent = await response.text();
remoteVersion = {
version: entry.version || '1.0.0',
timestamp: entry.lastModified,
author: entry.author || 'unknown',
hash: createHash('sha256').update(remoteContent).digest('hex'),
size: entry.size,
source: 'remote'
};
}
}
// Build comparison result
const result = {
element: elementName,
type: elementType,
local: localVersion,
remote: remoteVersion
};
if (localVersion && remoteVersion) {
result.status = localVersion.hash === remoteVersion.hash ? 'identical' : 'different';
if (showDiff && localContent && remoteContent && result.status === 'different') {
result.diff = await this.generateDiff(localContent, remoteContent);
}
}
else if (localVersion && !remoteVersion) {
result.status = 'local-only';
}
else if (!localVersion && remoteVersion) {
result.status = 'remote-only';
}
else {
result.status = 'not-found';
}
return {
success: true,
message: `Version comparison for '${elementName}' (${elementType})`,
data: result
};
}
catch (error) {
return {
success: false,
message: `Failed to compare versions: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Bulk download elements
*/
async bulkDownload(elementType, confirm) {
const config = this.configManager.getConfig();
if (!config.sync.bulk.download_enabled) {
return {
success: false,
message: 'Bulk download is not enabled in configuration'
};
}
// Get list of remote elements
const remoteResult = await this.listRemoteElements();
if (!remoteResult.success || !remoteResult.elements) {
return remoteResult;
}
// Filter by type if specified
let elementsToDownload = remoteResult.elements;
if (elementType) {
elementsToDownload = elementsToDownload.filter(e => e.type === elementType);
}
if (elementsToDownload.length === 0) {
return {
success: true,
message: 'No elements to download',
elements: []
};
}
// Show preview if required (unless already confirmed)
if (config.sync.bulk.require_preview && !confirm) {
return {
success: false,
message: `Bulk download preview:\n\n${elementsToDownload.length} elements will be downloaded:\n${elementsToDownload.map(e => `- ${e.name} (${e.type})`).join('\n')}\n\nTo proceed, use --confirm flag`,
data: { requiresConfirmation: true },
elements: elementsToDownload
};
}
// Perform actual bulk download
const results = {
downloaded: [],
skipped: [],
failed: []
};
for (const element of elementsToDownload) {
try {
const result = await this.downloadElement(element.name, element.type, undefined, true); // force=true to skip individual confirmations
if (result.success) {
results.downloaded.push(element.name);
}
else if (result.message?.includes('already up to date')) {
results.skipped.push(element.name);
}
else {
results.failed.push({ name: element.name, error: result.message || 'Unknown error' });
}
}
catch (error) {
results.failed.push({
name: element.name,
error: error instanceof Error ? error.message : String(error)
});
}
}
// Build summary message
let message = `Bulk download complete:\n`;
message += `- Downloaded: ${results.downloaded.length} elements\n`;
message += `- Skipped (up to date): ${results.skipped.length} elements\n`;
message += `- Failed: ${results.failed.length} elements`;
if (results.failed.length > 0) {
message += `\n\nFailed downloads:\n${results.failed.map(f => `- ${f.name}: ${f.error}`).join('\n')}`;
}
return {
success: results.failed.length === 0,
message,
data: results
};
}
/**
* Bulk upload elements
*/
async bulkUpload(elementType, confirm) {
const config = this.configManager.getConfig();
if (!config.sync.bulk.upload_enabled) {
return {
success: false,
message: 'Bulk upload is not enabled in configuration'
};
}
// Get list of local elements
const types = elementType ? [elementType] : [
ElementType.PERSONA,
ElementType.SKILL,
ElementType.TEMPLATE,
ElementType.AGENT,
ElementType.MEMORY,
ElementType.ENSEMBLE
];
const localElements = [];
for (const type of types) {
const dir = this.portfolioManager.getElementDir(type);
try {
const files = await this.fileOperations.listDirectory(dir);
for (const file of files) {
if (file.endsWith('.md')) {
localElements.push({
name: file.replace('.md', ''),
type,
path: path.join(dir, file)
});
}
}
}
catch {
// Directory may not exist yet
logger.debug(`Directory for ${type} does not exist yet`);
}
}
if (localElements.length === 0) {
return {
success: true,
message: 'No local elements to upload',
elements: []
};
}
// Show preview if required (unless already confirmed)
if (config.sync.bulk.require_preview && !confirm) {
// Convert to SyncElementInfo format for preview
const previewElements = localElements.map(e => ({
name: e.name,
type: e.type,
status: 'local-only',
action: 'upload'
}));
return {
success: false,
message: `Bulk upload preview:\n\n${localElements.length} elements will be uploaded:\n${localElements.map(e => `- ${e.name} (${e.type})`).join('\n')}\n\nTo proceed, use --confirm flag`,
data: { requiresConfirmation: true },
elements: previewElements
};
}
// Perform actual bulk upload
const results = {
uploaded: [],
skipped: [],
failed: []
};
for (const element of localElements) {
try {
const result = await this.uploadElement(element.name, element.type, true); // confirm=true to skip individual confirmations
if (result.success) {
results.uploaded.push(element.name);
}
else if (result.message?.includes('local-only')) {
results.skipped.push(element.name);
}
else {
results.failed.push({ name: element.name, error: result.message || 'Unknown error' });
}
}
catch (error) {
results.failed.push({
name: element.name,
error: error instanceof Error ? error.message : String(error)
});
}
}
// Build summary message
let message = `Bulk upload complete:\n`;
message += `- Uploaded: ${results.uploaded.length} elements\n`;
message += `- Skipped (local-only): ${results.skipped.length} elements\n`;
message += `- Failed: ${results.failed.length} elements`;
if (results.failed.length > 0) {
message += `\n\nFailed uploads:\n${results.failed.map(f => `- ${f.name}: ${f.error}`).join('\n')}`;
}
return {
success: results.failed.length === 0,
message,
data: results
};
}
/**
* Generate diff between two content versions
*/
async generateDiff(local, remote) {
// Simple line-based diff for now
const localLines = local.split('\n');
const remoteLines = remote.split('\n');
let diff = '';
const maxLines = Math.max(localLines.length, remoteLines.length);
for (let i = 0; i < maxLines && i < 10; i++) { // Show first 10 lines of diff
const localLine = localLines[i] || '';
const remoteLine = remoteLines[i] || '';
if (localLine !== remoteLine) {
if (localLine && !remoteLine) {
diff += `- ${localLine}\n`;
}
else if (!localLine && remoteLine) {
diff += `+ ${remoteLine}\n`;
}
else {
diff += `- ${localLine}\n`;
diff += `+ ${remoteLine}\n`;
}
}
}
if (maxLines > 10) {
diff += `\n... ${maxLines - 10} more lines ...`;
}
return diff || 'No differences found';
}
/**
* Build conflict metadata for UX/logging
*/
async buildConflictInfo(elementName, elementType, localPath, localContent, remoteEntry) {
const localMeta = this.extractMetadata(localContent);
let localModified = new Date();
try {
const stats = await this.fileOperations.stat(localPath);
localModified = stats.mtime;
}
catch {
// Ignore - use current date fallback
}
const remoteModified = remoteEntry.lastModified ? new Date(remoteEntry.lastModified) : new Date();
return {
element: elementName,
type: elementType,
localVersion: localMeta.version ?? 'unknown',
remoteVersion: remoteEntry.version ?? 'unknown',
localModified,
remoteModified
};
}
extractMetadata(content) {
try {
const parsed = SecureYamlParser.parse(content, {
maxYamlSize: 64 * 1024,
validateContent: false,
validateFields: false
});
return {
version: parsed.data?.version
};
}
catch {
return {};
}
}
/**
* Find a fuzzy match for an element name
*/
findFuzzyMatch(searchName, entries) {
const search = searchName.toLowerCase().replaceAll(/[-_]/g, '');
let bestMatch = null;
let bestScore = 0;
for (const entry of entries) {
// Normalize the entry name for comparison
const normalized = entry.name.toLowerCase().replaceAll(/[-_]/g, '');
// Calculate similarity score
const score = this.calculateSimilarity(search, normalized);
if (score > bestScore && score > 0.5) { // Minimum threshold of 0.5
bestScore = score;
bestMatch = entry;
}
}
return bestMatch;
}
/**
* Get suggestions for similar element names
*/
getSuggestions(searchName, entries) {
const search = searchName.toLowerCase().replaceAll(/[-_]/g, '');
const scored = [];
for (const entry of entries) {
const normalized = entry.name.toLowerCase().replaceAll(/[-_]/g, '');
const score = this.calculateSimilarity(search, normalized);
if (score > 0.3) { // Lower threshold for suggestions
scored.push({ entry, score });
}
}
// Sort by score and return top 5
return scored
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(s => ({ name: s.entry.name }));
}
/**
* Calculate similarity between two strings
* Returns a score between 0 and 1
*/
calculateSimilarity(a, b) {
// Exact match
if (a === b)
return 1.0;
// One contains the other
if (a.includes(b) || b.includes(a))
return 0.8;
// Calculate word overlap
const wordsA = a.split(/[^a-z0-9]+/);
const wordsB = b.split(/[^a-z0-9]+/);
let matches = 0;
for (const wordA of wordsA) {
if (wordA && wordsB.some(wordB => wordB === wordA)) {
matches++;
}
}
if (matches > 0) {
const overlap = (matches * 2) / (wordsA.length + wordsB.length);
return Math.max(0.6, overlap); // At least 0.6 for any word match
}
// Check for partial matches
for (const wordA of wordsA) {
for (const wordB of wordsB) {
if (wordA.length > 3 && wordB.length > 3) {
if (wordA.includes(wordB) || wordB.includes(wordA)) {
return 0.5;
}
}
}
}
// No significant similarity
return 0;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUG9ydGZvbGlvU3luY01hbmFnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcG9ydGZvbGlvL1BvcnRmb2xpb1N5bmNNYW5hZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7O0dBVUc7QUFFSCxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUM3QixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3BDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQU01QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSw0Q0FBNEMsQ0FBQztBQUM5RSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBNEV4RixNQUFNLE9BQU8sb0JBQW9CO0lBQ3ZCLGFBQWEsQ0FBZ0I7SUFDN0IsZ0JBQWdCLENBQW1CO0lBQ25DLFdBQVcsQ0FBdUI7SUFDbEMsT0FBTyxDQUF5QjtJQUNoQyxjQUFjLENBQXlCO0lBQ3ZDLFlBQVksQ0FBZTtJQUVuQyxZQUFZLFlBQThDO1FBQ3hELElBQUksQ0FBQyxhQUFhLEdBQUcsWUFBWSxDQUFDLGFBQWEsQ0FBQztRQUNoRCxJQUFJLENBQUMsZ0JBQWdCLEdBQUcsWUFBWSxDQUFDLGdCQUFnQixDQUFDO1FBQ3RELElBQUksQ0FBQyxXQUFXLEdBQUcsWUFBWSxDQUFDLG9CQUFvQixDQUFDO1FBQ3JELElBQUksQ0FBQyxPQUFPLEdBQUcsWUFBWSxDQUFDLE9BQU8sQ0FBQztRQUNwQyxJQUFJLENBQUMsY0FBYyxHQUFHLFlBQVksQ0FBQyxjQUFjLENBQUM7UUFDbEQsSUFBSSxDQUFDLFlB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