Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
PersonaManager.tsโ€ข10.8 kB
/** * Core persona management operations */ import * as path from 'path'; import matter from 'gray-matter'; import { Persona, PersonaMetadata } from '../types/persona.js'; import { PersonaLoader } from './PersonaLoader.js'; import { PersonaValidator } from './PersonaValidator.js'; import { validateFilename, sanitizeInput } from '../security/InputValidator.js'; import { generateAnonymousId, generateUniqueId, slugify } from '../utils/filesystem.js'; import { IndicatorConfig, formatIndicator } from '../config/indicator-config.js'; export class PersonaManager { private personas: Map<string, Persona> = new Map(); private activePersona: string | null = null; private currentUser: string | null = null; private loader: PersonaLoader; private validator: PersonaValidator; private indicatorConfig: IndicatorConfig; constructor(personasDir: string, indicatorConfig: IndicatorConfig) { this.loader = new PersonaLoader(personasDir); this.validator = new PersonaValidator(); this.indicatorConfig = indicatorConfig; } /** * Initialize and load all personas */ async initialize(): Promise<void> { this.personas = await this.loader.loadAll(() => this.getCurrentUserForAttribution()); } /** * Reload all personas from disk */ async reload(): Promise<void> { this.personas.clear(); this.activePersona = null; await this.initialize(); } /** * Get all loaded personas */ getAllPersonas(): Map<string, Persona> { return this.personas; } /** * Find a persona by identifier (filename, name, or unique_id) */ findPersona(identifier: string): Persona | undefined { // Try direct filename match let persona = this.personas.get(identifier); if (!persona) { // Try with .md extension persona = this.personas.get(`${identifier}.md`); } if (!persona) { // Search by name (case-insensitive) persona = Array.from(this.personas.values()).find(p => p.metadata.name.toLowerCase() === identifier.toLowerCase() ); } if (!persona) { // Search by unique_id persona = Array.from(this.personas.values()).find(p => p.unique_id === identifier ); } return persona; } /** * Activate a persona */ activatePersona(identifier: string): { success: boolean; message: string; persona?: Persona } { const persona = this.findPersona(identifier); if (!persona) { return { success: false, message: `Persona not found: "${identifier}"` }; } this.activePersona = persona.filename; return { success: true, message: `Activated persona: ${persona.metadata.name}`, persona }; } /** * Deactivate the current persona */ deactivatePersona(): { success: boolean; message: string } { if (!this.activePersona) { return { success: false, message: "No persona is currently active" }; } const persona = this.personas.get(this.activePersona); const personaName = persona?.metadata.name || this.activePersona; this.activePersona = null; return { success: true, message: `Deactivated persona: ${personaName}` }; } /** * Get the active persona */ getActivePersona(): Persona | null { if (!this.activePersona) return null; return this.personas.get(this.activePersona) || null; } /** * Get persona indicator for responses */ getPersonaIndicator(): string { if (!this.activePersona) return ""; const persona = this.personas.get(this.activePersona); if (!persona) return ""; return formatIndicator(this.indicatorConfig, { name: persona.metadata.name, version: persona.metadata.version, author: persona.metadata.author, category: persona.metadata.category }); } /** * Create a new persona */ async createPersona( name: string, description: string, category: string, instructions: string ): Promise<{ success: boolean; message: string; filename?: string }> { try { // Validate inputs const cleanName = sanitizeInput(name, 50); const cleanDescription = sanitizeInput(description, 200); if (!cleanName) { return { success: false, message: "Persona name cannot be empty" }; } // Generate filename const baseFilename = slugify(cleanName) + '.md'; const filename = validateFilename(baseFilename); // Check if already exists if (this.personas.has(filename)) { return { success: false, message: `A persona named "${cleanName}" already exists` }; } // Create metadata const metadata: PersonaMetadata = { name: cleanName, description: cleanDescription, category: category || 'general', version: '1.0', author: this.getCurrentUserForAttribution() || undefined, unique_id: generateUniqueId(cleanName, this.getCurrentUserForAttribution() || undefined), triggers: this.generateTriggers(cleanName), created_date: new Date().toISOString() }; // Create persona const persona: Persona = { metadata, content: instructions, filename, unique_id: metadata.unique_id! }; // Validate const validation = this.validator.validatePersona(persona); if (!validation.valid) { return { success: false, message: `Validation failed: ${validation.issues.join(', ')}` }; } // Save to disk await this.loader.savePersona(persona); // Add to memory this.personas.set(filename, persona); return { success: true, message: `Created persona "${cleanName}" successfully`, filename }; } catch (error) { return { success: false, message: `Failed to create persona: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Edit an existing persona */ async editPersona( personaName: string, field: string, value: string ): Promise<{ success: boolean; message: string }> { const persona = this.findPersona(personaName); if (!persona) { return { success: false, message: `Persona not found: "${personaName}"` }; } try { const oldVersion = String(persona.metadata.version || '1.0'); switch (field.toLowerCase()) { case 'name': persona.metadata.name = sanitizeInput(value, 50); break; case 'description': persona.metadata.description = sanitizeInput(value, 200); break; case 'instructions': case 'content': persona.content = value; break; case 'category': persona.metadata.category = value; break; case 'triggers': persona.metadata.triggers = value.split(',').map(t => t.trim()).filter(t => t); break; case 'version': persona.metadata.version = value; break; default: return { success: false, message: `Unknown field: "${field}". Valid fields: name, description, instructions, category, triggers, version` }; } // Auto-increment version if not explicitly setting version if (field !== 'version') { persona.metadata.version = this.incrementVersion(oldVersion); } // Validate const validation = this.validator.validatePersona(persona); if (!validation.valid) { return { success: false, message: `Validation failed: ${validation.issues.join(', ')}` }; } // Save changes await this.loader.savePersona(persona); return { success: true, message: `Updated ${field} for "${persona.metadata.name}" (v${persona.metadata.version})` }; } catch (error) { return { success: false, message: `Failed to edit persona: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate a persona */ validatePersona(identifier: string): { found: boolean; validation?: ReturnType<PersonaValidator['validatePersona']> } { const persona = this.findPersona(identifier); if (!persona) { return { found: false }; } return { found: true, validation: this.validator.validatePersona(persona) }; } /** * Set current user identity */ setUserIdentity(username: string | null, email?: string): void { this.currentUser = username; if (username) { process.env.DOLLHOUSE_USER = username; if (email) { process.env.DOLLHOUSE_EMAIL = email; } } else { delete process.env.DOLLHOUSE_USER; delete process.env.DOLLHOUSE_EMAIL; } } /** * Get current user identity */ getUserIdentity(): { username: string | null; email: string | null } { return { username: process.env.DOLLHOUSE_USER || null, email: process.env.DOLLHOUSE_EMAIL || null }; } /** * Clear user identity */ clearUserIdentity(): void { this.setUserIdentity(null); } /** * Update indicator configuration */ updateIndicatorConfig(config: IndicatorConfig): void { this.indicatorConfig = config; } /** * Get current indicator configuration */ getIndicatorConfig(): IndicatorConfig { return this.indicatorConfig; } /** * Helper to get current user for attribution */ private getCurrentUserForAttribution(): string { return this.currentUser || process.env.DOLLHOUSE_USER || generateAnonymousId(); } /** * Generate trigger keywords from persona name */ private generateTriggers(name: string): string[] { const words = name.toLowerCase().split(/\s+/); const triggers = [...words]; // Add the full name as a trigger if (words.length > 1) { triggers.push(name.toLowerCase()); } // Remove duplicates return [...new Set(triggers)].filter(t => t.length > 2); } /** * Increment version number */ private incrementVersion(version: string | number): string { // Handle both string and number versions const versionStr = String(version); const parts = versionStr.split('.'); // If it's just a number like "1", make it "1.0" if (parts.length === 1) { parts.push('0'); } const patch = Number.parseInt(parts[parts.length - 1]) || 0; parts[parts.length - 1] = (patch + 1).toString(); return parts.join('.'); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server