@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.
341 lines • 13.5 kB
TypeScript
/**
* Core persona management operations
*/
import { Persona } from '../types/persona.js';
import { PersonaElement, PersonaElementMetadata } from './PersonaElement.js';
import { FileLockManager } from '../security/fileLockManager.js';
import { IndicatorConfig } from '../config/indicator-config.js';
import { PortfolioManager } from '../portfolio/PortfolioManager.js';
import { StateChangeNotifier } from '../services/StateChangeNotifier.js';
import { PersonaImporter, ImportResult } from './export-import/PersonaImporter.js';
import { BaseElementManager, type BaseElementManagerOptions } from '../elements/base/BaseElementManager.js';
import { ValidationRegistry } from '../services/validation/ValidationRegistry.js';
import { MetadataService } from '../services/MetadataService.js';
import { FileOperationsService } from '../services/FileOperationsService.js';
interface PersonaManagerOptions extends BaseElementManagerOptions {
personaImporter?: PersonaImporter;
notifier?: StateChangeNotifier;
}
export declare class PersonaManager extends BaseElementManager<PersonaElement> {
/**
* Track active personas by filename (stable identifier)
* Issue #281: Changed from single active persona to Set for multiple active
*/
private activePersonas;
private currentUser;
private indicatorConfig;
protected portfolioManager: PortfolioManager;
protected fileLockManager: FileLockManager;
private personaImporter?;
private notifier?;
private readonly personasDir;
private pathValidatorInitialized;
private triggerValidationService;
private validationService;
private metadataService;
private readonly serializationService;
constructor(portfolioManager: PortfolioManager, indicatorConfig: IndicatorConfig, fileLockManager: FileLockManager, fileOperationsService: FileOperationsService, validationRegistry: ValidationRegistry, metadataService: MetadataService, personaImporter?: PersonaImporter, notifier?: StateChangeNotifier, baseOptions?: PersonaManagerOptions);
/**
* Override to return singular form for element type labeling.
*/
protected getElementLabel(): string;
/**
* Initialize and load all personas
*/
initialize(): Promise<void>;
/**
* Reload personas using BaseElementManager caches
*/
reload(): Promise<void>;
private deriveFilename;
private clonePersona;
private normalizePersonaForSave;
/**
* Get all loaded personas as a Map (used by import operations)
*/
getAllPersonas(): Promise<Map<string, Persona>>;
/**
* Get personas from cache as a Map (synchronous, for DI consumers)
*/
getPersonas(): Map<string, Persona>;
/**
* List all personas as an array
* Ensures all personas have filenames set
*/
list(): Promise<PersonaElement[]>;
/**
* Reload personas and return MCP-formatted response
*/
reloadPersonas(): Promise<{
content: {
type: string;
text: string;
}[];
}>;
/**
* Find a persona by identifier (filename, name, or unique_id)
* Uses cached elements from BaseElementManager for synchronous access
* Multi-strategy search: filename (with/without .md), name (case-insensitive), unique_id
*/
/** Cache miss metrics for monitoring persona lookup health (Issue #843) */
private cacheMissMetrics;
/** Get cache miss metrics for monitoring and diagnostics */
getCacheMissMetrics(): Readonly<typeof this.cacheMissMetrics>;
/**
* Check if a persona matches an identifier by filename, name, or ID.
* Shared matching logic used by both findPersona() and findPersonaAsync().
*/
private matchesIdentifier;
findPersona(identifier: string): PersonaElement | undefined;
/** In-flight disk lookups to prevent duplicate reads for the same identifier */
private pendingLookups;
/**
* Find a persona with disk fallback when cache misses.
*
* Issue #843: findPersona() is synchronous and cache-only. After LRU eviction
* or direct filesystem edits followed by scan(), personas become invisible even
* though the file exists on disk. This async method tries the fast cache path
* first, then falls back to BaseElementManager.findByName() which has the full
* cache → storage index → direct file load → list scan fallback chain.
*
* Includes request deduplication: concurrent lookups for the same identifier
* share a single disk read to avoid redundant I/O in bridge/swarm scenarios.
*/
findPersonaAsync(identifier: string): Promise<PersonaElement | undefined>;
/**
* Perform the actual disk lookup for findPersonaAsync.
* Separated to support request deduplication.
*/
private performDiskLookup;
/**
* Activate a persona
* Issue #281: Now supports multiple active personas (adds to set instead of replacing)
* Issue #843: Now async — uses findPersonaAsync() to recover from cache eviction
*/
activatePersona(identifier: string): Promise<{
success: boolean;
message: string;
persona?: Persona;
}>;
/**
* Deactivate a specific persona by name
* Issue #281: Now requires a name parameter to deactivate specific persona from the set
*/
deactivatePersona(identifier?: string): {
success: boolean;
message: string;
};
/**
* Get the first active persona (for backward compatibility)
* Issue #281: With multiple active personas, returns the first one
*/
getActivePersona(): PersonaElement | null;
/**
* Get all active personas
* Issue #281: New method to get all active personas
*/
getActivePersonas(): PersonaElement[];
/**
* Get identifier for the first active persona (filename).
* Issue #281: For backward compatibility, returns first active persona ID
*/
getActivePersonaId(): string | null;
/**
* Get all active persona IDs (filenames)
* Issue #281: New method to get all active persona IDs
*/
getActivePersonaIds(): string[];
/**
* Check if a specific persona is active
* Issue #281: New method to check if a persona is active
*/
isPersonaActive(identifier: string): boolean;
/**
* Get persona indicator for responses
* Issue #281: Now supports multiple active personas - concatenates all indicators
*/
getPersonaIndicator(): string;
/**
* Build persona metadata with default values and optional overrides
*
* @param validatedInputs - Validated input data
* @param metadataOverrides - Optional metadata overrides from user
* @returns Complete PersonaMetadata object
*/
private buildPersonaMetadata;
/**
* Validate and sanitize all persona creation inputs
* SECURITY: Phase 4.3 - Added Unicode validation to all text inputs
* SECURITY: Phase 4.1 - Block ALL invalid content, not just critical
* SECURITY: Phase 4.7 - Accept normalized content even if Unicode issues were detected and fixed
*
* @param name - Raw persona name
* @param description - Raw persona description
* @param instructions - Raw persona instructions
* @param triggers - Optional comma-separated trigger words
* @returns Validated and sanitized inputs ready for metadata construction
* @throws Error if validation fails
*/
private validatePersonaInputs;
/**
* Validates metadata keys to prevent prototype pollution
* @throws Error if dangerous keys are detected
* @private
*/
private validateMetadataKeys;
/**
* Create a new persona following the unified element manager pattern.
*
* This is the primary API for persona creation, replacing the deprecated
* `createPersona()` and `createNewPersona()` methods (v2 breaking change).
*
* @param data - Persona creation options
* @param data.name - Display name for the persona (required)
* @param data.description - Short description of the persona's purpose
* @param data.instructions - Behavioral instructions (alias: content)
* @param data.category - Optional category for organization
* @param data.triggers - Optional keywords that activate this persona
* @returns The created PersonaElement
* @throws {Error} If validation fails or persona already exists
*
* @example
* // Basic persona creation
* const persona = await personaManager.create({
* name: 'Code Reviewer',
* description: 'Expert at reviewing code for quality',
* instructions: 'You are a meticulous code reviewer...'
* });
*
* @example
* // With category and triggers
* const persona = await personaManager.create({
* name: 'Technical Writer',
* description: 'Specializes in clear documentation',
* instructions: 'You write clear, concise documentation...',
* category: 'professional',
* triggers: ['docs', 'documentation', 'readme']
* });
*
* @example
* // Minimal creation (defaults applied)
* const persona = await personaManager.create({
* name: 'Quick Helper'
* });
*
* @since v2.0.0 - Replaces createPersona() and createNewPersona()
*/
create(data: Partial<PersonaElementMetadata> & {
content?: string;
instructions?: string;
}): Promise<PersonaElement>;
/**
* Edit an existing persona
*/
editPersona(personaIdentifier: string, field: string, value: string): Promise<{
success: boolean;
message: string;
isDefault: boolean;
newName: string;
version: string | undefined;
newId: string | undefined;
}>;
/**
* Edit existing persona and return the updated persona (for PersonaHandler compatibility)
*/
editExistingPersona(persona: PersonaElement, field: string, value: string): Promise<PersonaElement>;
/**
* Validate a persona and return a formatted report
*/
validatePersona(personaIdentifier: string): {
success: boolean;
message: string;
report: import("../index.barrel.js").ElementValidationResult;
};
/**
* Set current user identity
* SECURITY: Phase 4.6 - Added username and email validation
*/
setUserIdentity(username: string | null, email?: string): void;
/**
* Get current user identity
*/
getUserIdentity(): {
username: string | null;
email: string | null;
};
/**
* Clear user identity
*/
clearUserIdentity(): void;
/**
* Update indicator configuration
*/
updateIndicatorConfig(config: IndicatorConfig): void;
/**
* Get current indicator configuration
*/
getIndicatorConfig(): IndicatorConfig;
/**
* Helper to get current user for attribution
* REFACTORED: Now delegates to MetadataService for consistent user attribution
*/
getCurrentUserForAttribution(): string;
/**
* Reset internal state; aligns PersonaManager with DI lifecycle hooks.
* CRITICAL: Must call super.dispose() to clean up file watchers and prevent open handles
*/
dispose(): void;
/**
* Check if active set cleanup is needed and perform cleanup if necessary
* Issue #83: Active element limit enforcement for personas
* @private
*/
private checkAndCleanupActiveSet;
/**
* Clean up stale entries from active personas set
* Issue #83: Validates that all active personas still exist and removes orphaned references
* @private
*/
private cleanupStaleActivePersonas;
private notifyPersonaChange;
private initializePathValidator;
/**
* Override delete to handle auto-deactivation before deletion.
* FIX: Issue #281 - Unified delete pattern for standard element-crud flow
*
* @param filePath - The filename of the persona to delete
*/
delete(filePath: string): Promise<void>;
/**
* Delete persona by name or identifier (legacy API)
* @deprecated Use delete(filename) via standard element-crud flow
*/
deletePersona(personaIdentifier: string): Promise<{
success: boolean;
message: string;
}>;
importPersona(source: string, overwrite?: boolean): Promise<ImportResult>;
/**
* Convert PersonaMetadata (legacy) to PersonaElementMetadata (new standard)
* Ensures strict typing for age_rating and generation_method
* NOTE: Filename is infrastructure data, not business metadata - it's excluded here
*/
private toPersonaElementMetadata;
/**
* Type guard to check if metadata is PersonaElementMetadata
*/
private isPersonaElementMetadata;
protected parseMetadata(data: any): Promise<PersonaElementMetadata>;
protected createElement(metadata: PersonaElementMetadata, bodyText: string): PersonaElement;
protected serializeElement(element: PersonaElement): Promise<string>;
getFileExtension(): string;
importElement(data: string, format?: 'json' | 'yaml' | 'markdown'): Promise<PersonaElement>;
exportElement(persona: PersonaElement, _format?: 'json' | 'yaml' | 'markdown'): Promise<string>;
protected canDelete(element: PersonaElement): Promise<{
allowed: boolean;
reason?: string;
}>;
protected beforeSave(persona: PersonaElement): Promise<void>;
}
export {};
//# sourceMappingURL=PersonaManager.d.ts.map