Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
PersonaElement.tsโ€ข10.2 kB
/** * Persona element class implementing IElement interface. * Represents a behavioral profile that defines AI personality and interaction style. */ import { BaseElement } from '../elements/BaseElement.js'; import { IElement, IElementMetadata, ElementValidationResult } from '../types/elements/index.js'; import { ElementType } from '../portfolio/types.js'; import { PersonaMetadata } from '../types/persona.js'; import { logger } from '../utils/logger.js'; import matter from 'gray-matter'; // Extend IElementMetadata with persona-specific fields export interface PersonaElementMetadata extends IElementMetadata { unique_id?: string; // Legacy ID for backward compatibility triggers?: string[]; category?: string; age_rating?: 'all' | '13+' | '18+'; content_flags?: string[]; ai_generated?: boolean; generation_method?: 'human' | 'ChatGPT' | 'Claude' | 'hybrid'; price?: string; revenue_split?: string; license?: string; created_date?: string; } export class PersonaElement extends BaseElement implements IElement { public content: string; public filename: string; public declare metadata: PersonaElementMetadata; constructor(metadata: Partial<PersonaElementMetadata>, content: string = '', filename: string = '') { super(ElementType.PERSONA, metadata); this.content = content; this.filename = filename; // Ensure persona-specific metadata this.metadata = { ...this.metadata, triggers: metadata.triggers || [], category: metadata.category || 'personal', age_rating: metadata.age_rating || 'all', content_flags: metadata.content_flags || [], ai_generated: metadata.ai_generated || false, generation_method: metadata.generation_method || 'human', price: metadata.price || 'free', license: metadata.license || 'CC-BY-SA-4.0', created_date: metadata.created_date || new Date().toISOString().split('T')[0] }; } /** * Create PersonaElement from legacy Persona interface */ static fromLegacy(legacyPersona: { metadata: PersonaMetadata; content: string; filename: string; unique_id: string }): PersonaElement { const metadata: Partial<PersonaElementMetadata> = { name: legacyPersona.metadata.name, description: legacyPersona.metadata.description, author: legacyPersona.metadata.author, version: legacyPersona.metadata.version, triggers: legacyPersona.metadata.triggers, category: legacyPersona.metadata.category, age_rating: legacyPersona.metadata.age_rating, content_flags: legacyPersona.metadata.content_flags, ai_generated: legacyPersona.metadata.ai_generated, generation_method: legacyPersona.metadata.generation_method, price: legacyPersona.metadata.price, revenue_split: legacyPersona.metadata.revenue_split, license: legacyPersona.metadata.license, created_date: legacyPersona.metadata.created_date }; const persona = new PersonaElement(metadata, legacyPersona.content, legacyPersona.filename); // Preserve the legacy unique_id as the element id persona.id = legacyPersona.unique_id; return persona; } /** * Convert to legacy Persona interface for backward compatibility */ toLegacy(): { metadata: PersonaMetadata; content: string; filename: string; unique_id: string } { const legacyMetadata: PersonaMetadata = { name: this.metadata.name, description: this.metadata.description, unique_id: this.id, author: this.metadata.author, triggers: this.metadata.triggers, version: this.metadata.version, category: this.metadata.category, age_rating: this.metadata.age_rating, content_flags: this.metadata.content_flags, ai_generated: this.metadata.ai_generated, generation_method: this.metadata.generation_method, price: this.metadata.price, revenue_split: this.metadata.revenue_split, license: this.metadata.license, created_date: this.metadata.created_date }; return { metadata: legacyMetadata, content: this.content, filename: this.filename, unique_id: this.id }; } /** * Persona-specific validation */ public override validate(): ElementValidationResult { const result = super.validate(); // Initialize arrays if not present if (!result.errors) result.errors = []; if (!result.warnings) result.warnings = []; // Add persona-specific validation rules // Content should not be empty if (!this.content || this.content.trim().length === 0) { result.errors.push({ field: 'content', message: 'Persona content cannot be empty', code: 'EMPTY_CONTENT' }); } // Content should be reasonable length if (this.content && this.content.length > 10000) { result.warnings.push({ field: 'content', message: 'Persona content is very long, consider breaking it down', severity: 'medium' }); } // Triggers should be reasonable if (this.metadata.triggers && this.metadata.triggers.length > 10) { result.warnings.push({ field: 'triggers', message: 'Many triggers may cause activation conflicts', severity: 'medium' }); } // Check for adult content flags if (this.metadata.age_rating === '18+' && !this.metadata.content_flags?.includes('adult')) { result.warnings.push({ field: 'content_flags', message: '18+ content should include "adult" in content_flags', severity: 'low' }); } // Update the valid flag based on final errors result.valid = (result.errors?.length || 0) === 0; return result; } /** * Get content for serialization */ protected override getContent(): string { return this.content; } /** * Serialize persona to markdown format * Refactored to use base class pattern with getContent() */ public override serialize(): string { // Store original metadata const originalMetadata = this.metadata; // Add persona-specific fields to metadata temporarily // Cast to PersonaElementMetadata to include unique_id this.metadata = { ...originalMetadata, unique_id: this.id, // Include ID as unique_id for legacy compatibility triggers: (originalMetadata as PersonaElementMetadata).triggers, category: (originalMetadata as PersonaElementMetadata).category, age_rating: (originalMetadata as PersonaElementMetadata).age_rating, content_flags: (originalMetadata as PersonaElementMetadata).content_flags, ai_generated: (originalMetadata as PersonaElementMetadata).ai_generated, generation_method: (originalMetadata as PersonaElementMetadata).generation_method, price: (originalMetadata as PersonaElementMetadata).price, revenue_split: (originalMetadata as PersonaElementMetadata).revenue_split, license: (originalMetadata as PersonaElementMetadata).license, created_date: (originalMetadata as PersonaElementMetadata).created_date }; // Use base class serialize which now uses js-yaml const result = super.serialize(); // Restore original metadata this.metadata = originalMetadata; return result; } /** * Serialize to JSON format for internal use and testing */ public override serializeToJSON(): string { // Include persona-specific fields const data = { ...JSON.parse(super.serializeToJSON()), content: this.content }; return JSON.stringify(data, null, 2); } /** * Deserialize persona from markdown format */ public override deserialize(data: string): void { try { const parsed = matter(data); const metadata = parsed.data as PersonaMetadata; // Update metadata this.metadata = { ...this.metadata, name: metadata.name, description: metadata.description, author: metadata.author, version: metadata.version, triggers: metadata.triggers, category: metadata.category, age_rating: metadata.age_rating, content_flags: metadata.content_flags, ai_generated: metadata.ai_generated, generation_method: metadata.generation_method, price: metadata.price, revenue_split: metadata.revenue_split, license: metadata.license, created_date: metadata.created_date }; // Update content (trim to remove leading/trailing whitespace) this.content = parsed.content.trim(); // Update ID if provided if (metadata.unique_id) { this.id = metadata.unique_id; } this._isDirty = true; logger.debug(`Deserialized persona: ${this.metadata.name}`); } catch (error) { // Preserve original error context for better debugging const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; logger.error('Failed to deserialize persona', { error: errorMessage, stack: errorStack, data: data.substring(0, 200) // Log first 200 chars for context }); // Re-throw with original error as cause const deserializeError = new Error(`PersonaElement deserialization failed: ${errorMessage}`); if (error instanceof Error) { deserializeError.cause = error; } throw deserializeError; } } /** * Persona activation lifecycle */ public override async activate(): Promise<void> { logger.info(`Activating persona: ${this.metadata.name} (${this.id})`); // Personas don't need special activation logic currently // But this provides a hook for future enhancements await super.activate?.(); } /** * Persona deactivation lifecycle */ public override async deactivate(): Promise<void> { logger.info(`Deactivating persona: ${this.metadata.name} (${this.id})`); // Personas don't need special deactivation logic currently // But this provides a hook for future enhancements await super.deactivate?.(); } }

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