UNPKG

@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.

1,002 lines 138 kB
/** * ElementCRUDHandler - Handles all generic element CRUD operations * * Provides create, edit, validate, and delete operations for all element types * (personas, skills, templates, agents, memories). * * Uses dependency injection for all services: * - InitializationService for setup tasks * - PersonaIndicatorService for persona indicator formatting * - Element managers (SkillManager, TemplateManager, AgentManager, MemoryManager) * - PersonaManager for persona operations * - PortfolioManager for portfolio operations * * FIX: DMCP-SEC-006 - Security audit suppression * This handler delegates all operations to specialized element managers. * Audit logging happens in the element managers themselves. * @security-audit-suppress DMCP-SEC-006 */ import { ElementType } from '../portfolio/PortfolioManager.js'; import os from 'node:os'; import path from 'node:path'; import { logger } from '../utils/logger.js'; import { ElementNotFoundError } from '../utils/ErrorHandler.js'; import { createElement as createElementCommand } from './element-crud/createElement.js'; import { deleteElement as deleteElementCommand } from './element-crud/deleteElement.js'; import { editElement as editElementCommand } from './element-crud/editElement.js'; import { upgradeElement as upgradeElementCommand } from './element-crud/upgradeElement.js'; import { listElements as listElementsCommand } from './element-crud/listElements.js'; import { findElementFlexibly, sanitizeMetadata as sanitizeMetadataRecord } from './element-crud/helpers.js'; import { validateElement as validateElementCommand } from './element-crud/validateElement.js'; import { SecurityMonitor } from '../security/securityMonitor.js'; import { ELEMENT_TYPE_MAP } from '../utils/elementTypeNormalization.js'; import { TemplateActivationStrategy, SkillActivationStrategy, AgentActivationStrategy, MemoryActivationStrategy, PersonaActivationStrategy, EnsembleActivationStrategy } from './strategies/index.js'; import { formatValidationFailedError } from './element-crud/responseFormatter.js'; import { findConfirmAdvisoryElements, findConfirmDenyingElement, getGatekeeperDiagnostics, } from './mcp-aql/policies/ElementPolicies.js'; export class ElementCRUDHandler { skillManager; templateManager; templateRenderer; agentManager; memoryManager; ensembleManager; personaManager; portfolioManager; initService; indicatorService; fileOperations; elementQueryService; validationRegistry; activationStore; backupService; policyExportService; strategies; constructor(skillManager, templateManager, templateRenderer, agentManager, memoryManager, ensembleManager, personaManager, portfolioManager, initService, indicatorService, fileOperations, elementQueryService, validationRegistry, activationStore, backupService, policyExportService) { this.skillManager = skillManager; this.templateManager = templateManager; this.templateRenderer = templateRenderer; this.agentManager = agentManager; this.memoryManager = memoryManager; this.ensembleManager = ensembleManager; this.personaManager = personaManager; this.portfolioManager = portfolioManager; this.initService = initService; this.indicatorService = indicatorService; this.fileOperations = fileOperations; this.elementQueryService = elementQueryService; this.validationRegistry = validationRegistry; this.activationStore = activationStore; this.backupService = backupService; this.policyExportService = policyExportService; // Initialize strategy map with all element type strategies this.strategies = new Map([ [ElementType.PERSONA, new PersonaActivationStrategy(personaManager, indicatorService)], [ElementType.SKILL, new SkillActivationStrategy(skillManager)], [ElementType.TEMPLATE, new TemplateActivationStrategy(templateManager)], [ElementType.AGENT, new AgentActivationStrategy(agentManager)], [ElementType.MEMORY, new MemoryActivationStrategy(memoryManager)], [ElementType.ENSEMBLE, new EnsembleActivationStrategy(ensembleManager, portfolioManager, skillManager, templateManager, agentManager, memoryManager, personaManager)] ]); } async ensureInitialized() { await this.initService.ensureInitialized(); } getPersonaIndicator() { return this.indicatorService.getPersonaIndicator(); } getContext() { return { ensureInitialized: () => this.ensureInitialized(), getPersonaIndicator: () => this.getPersonaIndicator(), skillManager: this.skillManager, templateManager: this.templateManager, templateRenderer: this.templateRenderer, agentManager: this.agentManager, memoryManager: this.memoryManager, ensembleManager: this.ensembleManager, portfolioManager: this.portfolioManager, personaManager: this.personaManager, fileOperations: this.fileOperations, elementQueryService: this.elementQueryService, validationRegistry: this.validationRegistry, backupService: this.backupService, }; } /** * Find an element by name, supporting both exact display name and filename (slug) matching * Helper method extracted from index.ts:346-379 */ async findElementFlexibly(name, elementList) { return findElementFlexibly(name, elementList); } /** * Sanitize metadata object to prevent prototype pollution * Helper method extracted from index.ts:390-410 */ sanitizeMetadata(metadata) { return sanitizeMetadataRecord(metadata); } normalizeLookupValue(value) { return typeof value === 'string' ? value.normalize('NFC').trim() : ''; } hasGatekeeperPolicy(metadata) { return Boolean(metadata?.['gatekeeper'] || getGatekeeperDiagnostics(metadata)); } toPolicyElementType(type) { const normalizedType = this.normalizeLookupValue(type).toLowerCase(); switch (normalizedType) { case 'personas': return 'persona'; case 'skills': return 'skill'; case 'agents': return 'agent'; case 'memories': return 'memory'; case 'ensembles': return 'ensemble'; default: return normalizedType; } } /** * Create a new element * Extracted from index.ts:1492-1631 (140 lines - exact copy) */ async createElement(args) { return createElementCommand(this.getContext(), args); } /** * Edit an existing element using GraphQL-aligned nested input objects. * * @example * await handler.editElement({ * name: 'my-skill', * type: 'skills', * input: { * description: 'Updated', * metadata: { triggers: ['code'] } * } * }); */ async editElement(args) { return editElementCommand(this.getContext(), args); } /** * Upgrade element from v1 single-body to v2 dual-field format (instructions + content) */ async upgradeElement(args) { return upgradeElementCommand(this.getContext(), args); } /** * Validate an element * Extracted from index.ts:1941-2054 (114 lines - exact copy) */ async validateElement(args) { return validateElementCommand(this.getContext(), args); } /** * Delete an element * Extracted from index.ts:2056-2310 (255 lines - exact copy, split for readability) */ async deleteElement(args) { return deleteElementCommand(this.getContext(), args); } normalizeElementType(type) { // Issue #501: Guard against null/undefined to match shared utility pattern if (type == null || typeof type !== 'string' || type.trim() === '') { return ''; } // If it's already a valid ElementType value, return as-is if (Object.values(ElementType).includes(type)) { return type; } // Use shared normalization map (Issue #433) const normalized = ELEMENT_TYPE_MAP[type.trim().toLowerCase()]; if (normalized) { return normalized; } // Unknown type - return as-is and let validation handle it return type; } async listElements(type, options) { return listElementsCommand(this.getContext(), type, options); } /** * Get raw elements array for a given type. * Unlike listElements which returns MCPResponse format, this returns raw element objects. * * @param type - Element type (persona, skill, template, agent, memory, ensemble) * @returns Array of raw element objects */ async getElements(type) { await this.ensureInitialized(); const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.personaManager.list(); case ElementType.SKILL: return this.skillManager.list(); case ElementType.TEMPLATE: return this.templateManager.list(); case ElementType.AGENT: return this.agentManager.list(); case ElementType.MEMORY: return this.memoryManager.list(); case ElementType.ENSEMBLE: return this.ensembleManager.list(); default: return []; } } async activateElement(name, type, context) { try { // FIX: DMCP-SEC-006 - Add security audit logging for element activation SecurityMonitor.logSecurityEvent({ type: 'ELEMENT_ACTIVATED', severity: 'LOW', source: 'ElementCRUDHandler.activateElement', details: `Element activation requested: ${type}/${name}`, additionalData: { elementType: type, elementName: name, contextProvided: !!context } }); const normalizedType = this.normalizeElementType(type); const strategy = this.strategies.get(normalizedType); if (!strategy) { return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } const result = await strategy.activate(name, context); // Issue #598: Persist activation state for session restore if (this.activationStore) { const filename = normalizedType === ElementType.PERSONA ? this.personaManager.findPersona(name)?.filename : undefined; this.activationStore.recordActivation(normalizedType, name, filename); } // Issue #762: Export policies to bridge after activation this.policyExportService?.exportPolicies().catch(() => { }); return result; } catch (error) { logger.error(`Failed to activate element:`, { type, name, error }); return { content: [{ type: "text", text: `❌ Failed to activate ${type} '${name}': ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async getActiveElements(type) { try { // Issue #501: When type is omitted, aggregate active elements across all types if (!type || type.trim() === '') { return this.aggregateActiveElements(); } const normalizedType = this.normalizeElementType(type); const strategy = this.strategies.get(normalizedType); if (!strategy) { return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } return await strategy.getActiveElements(); } catch (error) { logger.error(`Failed to get active elements:`, { type, error }); return { content: [{ type: "text", text: `❌ Failed to get active ${type || 'all'}: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Get raw active elements for Gatekeeper policy evaluation. * Returns active personas, skills, and ensembles with their metadata * mapped to the shape expected by the Gatekeeper's ActiveElement interface. * * Issue #452: Provides active element context for enforce() policy checks. */ async getActiveElementsForPolicy() { const result = []; const seen = new Set(); try { // Active personas (sync) const personas = this.personaManager.getActivePersonas(); for (const p of personas) { const key = `persona:${p.metadata.name}`; seen.add(key); result.push({ type: 'persona', name: p.metadata.name, metadata: p.metadata, }); } } catch (error) { logger.warn('Failed to gather active personas for policy evaluation', { error }); } try { // Active skills (async) const skills = await this.skillManager.getActiveSkills(); for (const s of skills) { const key = `skill:${s.metadata.name}`; seen.add(key); result.push({ type: 'skill', name: s.metadata.name, metadata: s.metadata, }); } } catch (error) { logger.warn('Failed to gather active skills for policy evaluation', { error }); } try { // Active agents (async) const agents = await this.agentManager.getActiveAgents(); for (const agent of agents) { const key = `agent:${agent.metadata.name}`; seen.add(key); result.push({ type: 'agent', name: agent.metadata.name, metadata: agent.metadata, }); } } catch (error) { logger.warn('Failed to gather active agents for policy evaluation', { error }); } try { // Active ensembles (async) const ensembles = await this.ensembleManager.getActiveEnsembles(); for (const e of ensembles) { const ensembleKey = `ensemble:${e.metadata.name}`; seen.add(ensembleKey); result.push({ type: 'ensemble', name: e.metadata.name, metadata: e.metadata, }); // Issue #625 Phase 4: Resolve ensemble member gatekeeper policies const members = e.metadata?.elements; if (Array.isArray(members)) { for (const member of members) { const memberKey = `${member.element_type}:${member.element_name}`; if (seen.has(memberKey)) continue; // Already active individually try { const memberElements = await this.getElements(member.element_type); const found = memberElements.find(el => el?.metadata?.name === member.element_name); if (found?.metadata && found.metadata?.gatekeeper) { seen.add(memberKey); result.push({ type: member.element_type, name: member.element_name, metadata: found.metadata, }); } } catch { // Non-fatal: skip member if element type lookup fails } } } } } catch (error) { logger.warn('Failed to gather active ensembles for policy evaluation', { error }); } return result; } async getPolicyElementsForReport(sessionId) { const merged = new Map(); const addElement = (element, sessionIds = []) => { if (!this.hasGatekeeperPolicy(element.metadata)) { return; } const key = `${element.type}:${element.name}`; const existing = merged.get(key); if (existing) { sessionIds.forEach(id => existing.sessionIds.add(id)); return; } merged.set(key, { type: element.type, name: element.name, metadata: element.metadata, sessionIds: new Set(sessionIds), }); }; const currentSessionId = this.activationStore?.getSessionId(); const includeCurrentSession = !sessionId || !currentSessionId || currentSessionId === sessionId; if (includeCurrentSession) { for (const activeElement of await this.getActiveElementsForPolicy()) { addElement(activeElement, currentSessionId ? [currentSessionId] : []); } } if (this.activationStore?.isEnabled()) { const persistedStates = await this.activationStore.listPersistedActivationStates(sessionId); const catalogs = new Map(); const getCatalog = async (type) => { const normalizedType = this.normalizeElementType(type); if (!catalogs.has(normalizedType)) { catalogs.set(normalizedType, (await this.getElements(normalizedType))); } return catalogs.get(normalizedType) ?? []; }; const findPersistedElement = async (type, activation) => { const catalog = await getCatalog(type); const normalizedName = this.normalizeLookupValue(activation.name); const normalizedFilename = this.normalizeLookupValue(activation.filename); const found = catalog.find((element) => { const metadata = element.metadata ?? {}; const elementName = this.normalizeLookupValue(metadata['name']); const elementFilename = this.normalizeLookupValue(element.filename ?? metadata['filename'] ?? metadata['sourceFile']); return elementName === normalizedName || (normalizedFilename !== '' && elementFilename === normalizedFilename); }); if (!found?.metadata || !this.hasGatekeeperPolicy(found.metadata)) { return null; } return { type: this.toPolicyElementType(type), name: found.metadata['name'] ?? activation.name, metadata: found.metadata, }; }; for (const state of persistedStates) { await this.mergePersistedPolicyState(state, addElement, findPersistedElement); } } return Array.from(merged.values()).map((entry) => ({ type: entry.type, name: entry.name, metadata: entry.metadata, ...(entry.sessionIds.size > 0 ? { sessionIds: Array.from(entry.sessionIds).sort((a, b) => a.localeCompare(b)) } : {}), })); } async releaseDeadlock() { const activeElements = await this.collectActiveElementsForDeadlockRelief(); const activePolicyElements = await this.getActiveElementsForPolicy(); const sandboxingElement = findConfirmDenyingElement(activePolicyElements); const advisoryElements = findConfirmAdvisoryElements(activePolicyElements); const deactivated = []; const failed = []; for (const element of activeElements) { const strategy = this.strategies.get(element.type); if (!strategy) { failed.push({ type: element.type, name: element.name, error: `No activation strategy registered for type '${element.type}'`, }); continue; } try { await strategy.deactivate(element.name); deactivated.push(element); } catch (error) { failed.push({ type: element.type, name: element.name, error: error instanceof Error ? error.message : String(error), }); } } const persistedStateCleared = Boolean(this.activationStore?.isEnabled()); const snapshotFile = await this.writeDeadlockReliefSnapshot({ sessionId: this.activationStore?.getSessionId(), activeBeforeReset: activeElements, deactivated, failed, likelyDeadlockCause: { ...(sandboxingElement ? { sandboxingElement } : {}), advisoryElements, }, persistedStateCleared, }); this.activationStore?.clearAll(); this.policyExportService?.exportPolicies().catch(() => { }); const failureSummary = failed.length > 0 ? ` with ${failed.length} failure(s)` : ''; SecurityMonitor.logSecurityEvent({ type: 'ELEMENT_DEACTIVATED', severity: failed.length > 0 ? 'MEDIUM' : 'LOW', source: 'ElementCRUDHandler.releaseDeadlock', details: `Deadlock relief deactivated ${deactivated.length} element(s)${failureSummary}`, additionalData: { sessionId: this.activationStore?.getSessionId(), activeBeforeReset: activeElements, deactivated, failed, persistedStateCleared, likelyDeadlockCause: { ...(sandboxingElement ? { sandboxingElement } : {}), advisoryElements, }, snapshotFile, }, }); return { ...(this.activationStore?.getSessionId() ? { sessionId: this.activationStore.getSessionId() } : {}), activeBeforeReset: activeElements, deactivated, failed, persistedStateCleared, likelyDeadlockCause: { ...(sandboxingElement ? { sandboxingElement } : {}), advisoryElements, }, ...(snapshotFile ? { snapshotFile } : {}), }; } async collectActiveElementsForDeadlockRelief() { const activeElements = []; const activePersonas = this.personaManager.getActivePersonas(); activeElements.push(...activePersonas.map((persona) => ({ type: ElementType.PERSONA, name: persona.metadata.name, }))); const activeSkills = await this.skillManager.getActiveSkills(); activeElements.push(...activeSkills.map((skill) => ({ type: ElementType.SKILL, name: skill.metadata.name, }))); const activeAgents = await this.agentManager.getActiveAgents(); activeElements.push(...activeAgents.map((agent) => ({ type: ElementType.AGENT, name: agent.metadata.name, }))); const activeMemories = await this.memoryManager.getActiveMemories(); activeElements.push(...activeMemories.map((memory) => ({ type: ElementType.MEMORY, name: memory.metadata.name, }))); const activeEnsembles = await this.ensembleManager.getActiveEnsembles(); activeElements.push(...activeEnsembles.map((ensemble) => ({ type: ElementType.ENSEMBLE, name: ensemble.metadata.name, }))); const seen = new Set(); return activeElements.filter((element) => { const key = `${element.type}:${element.name}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } async writeDeadlockReliefSnapshot(snapshot) { const snapshotDir = path.join(os.homedir(), '.dollhouse', 'state', 'deadlock-relief'); const safeSessionId = (snapshot.sessionId ?? 'session') .replaceAll(/[^a-zA-Z0-9_-]/g, '-') .slice(0, 64); const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-'); const snapshotFile = path.join(snapshotDir, `deadlock-relief-${safeSessionId}-${timestamp}.json`); try { await this.fileOperations.createDirectory(snapshotDir); await this.fileOperations.writeFile(snapshotFile, JSON.stringify({ createdAt: new Date().toISOString(), ...snapshot, }, null, 2), { source: 'ElementCRUDHandler.releaseDeadlock' }); return snapshotFile; } catch (error) { logger.warn('Failed to write deadlock relief snapshot', { snapshotFile, error: error instanceof Error ? error.message : String(error), }); return undefined; } } async mergePersistedPolicyState(state, addElement, findPersistedElement) { const pending = []; for (const [type, activations] of Object.entries(state.activations)) { for (const activation of activations ?? []) { pending.push((async () => { const found = await findPersistedElement(type, activation); if (found) { addElement(found, [state.sessionId]); } })()); } } if (pending.length === 0) { return; } await Promise.allSettled(pending); } async deactivateElement(name, type) { try { SecurityMonitor.logSecurityEvent({ type: 'ELEMENT_DEACTIVATED', severity: 'LOW', source: 'ElementCRUDHandler.deactivateElement', details: `Element deactivation requested: ${type}/${name}`, additionalData: { elementType: type, elementName: name } }); const normalizedType = this.normalizeElementType(type); const strategy = this.strategies.get(normalizedType); if (!strategy) { return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } const result = await strategy.deactivate(name); // Issue #598: Persist deactivation state for session restore if (this.activationStore) { this.activationStore.recordDeactivation(normalizedType, name); } // Issue #762: Export policies to bridge after deactivation this.policyExportService?.exportPolicies().catch(() => { }); return result; } catch (error) { // Re-throw ElementNotFoundError to propagate to MCP-AQL layer // This ensures operations return success=false instead of success=true with error text // Issue #275: Handlers return success=true for missing elements if (error instanceof ElementNotFoundError) { throw error; } // Also re-throw validation errors (e.g., missing required parameters) // so they result in success=false instead of success=true with error content if (error instanceof Error && error.message.includes('parameter is required')) { throw error; } logger.error(`Failed to deactivate element:`, { type, name, error }); return { content: [{ type: "text", text: `❌ Failed to deactivate ${type} '${name}': ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Issue #708: Get the element manager for a normalized type. * Used to check invalid element records when an element is "not found". */ getManagerForType(normalizedType) { switch (normalizedType) { case ElementType.PERSONA: return this.personaManager; case ElementType.SKILL: return this.skillManager; case ElementType.TEMPLATE: return this.templateManager; case ElementType.AGENT: return this.agentManager; case ElementType.MEMORY: return this.memoryManager; case ElementType.ENSEMBLE: return this.ensembleManager; default: return undefined; } } async getElementDetails(name, type) { try { const normalizedType = this.normalizeElementType(type); const strategy = this.strategies.get(normalizedType); if (!strategy) { return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } return await strategy.getElementDetails(name); } catch (error) { // Issue #708: When element is "not found", check if it actually exists // on disk but failed validation. Return a distinct error in that case. if (error instanceof ElementNotFoundError) { const normalizedType = this.normalizeElementType(type); const manager = this.getManagerForType(normalizedType); if (manager && typeof manager.getInvalidElement === 'function') { const invalidRecord = manager.getInvalidElement(name); if (invalidRecord) { return formatValidationFailedError(normalizedType, name, invalidRecord.reason, invalidRecord.filePath); } } throw error; } logger.error(`Failed to get element details:`, { type, name, error }); return { content: [{ type: "text", text: `❌ Failed to get ${type} details for '${name}': ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Reload elements of a specific type from the filesystem * Extracted from index.ts:609-679 (exact copy, adapted for handler pattern) */ async reloadElements(type) { try { // Normalize the type to handle both plural and singular forms const normalizedType = this.normalizeElementType(type); switch (normalizedType) { case ElementType.PERSONA: return this.personaManager.reloadPersonas(); case ElementType.SKILL: { this.skillManager.clearCache(); const skills = await this.skillManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${skills.length} skills from portfolio` }] }; } case ElementType.TEMPLATE: { // Template manager doesn't have clearCache, just list const templates = await this.templateManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${templates.length} templates from portfolio` }] }; } case ElementType.AGENT: { // Agent manager doesn't have clearCache, just list const agents = await this.agentManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${agents.length} agents from portfolio` }] }; } case ElementType.MEMORY: { // Memory manager doesn't have clearCache, just list const memories = await this.memoryManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${memories.length} memories from portfolio` }] }; } case ElementType.ENSEMBLE: { // Ensemble manager doesn't have clearCache, just list const ensembles = await this.ensembleManager.list(); return { content: [{ type: "text", text: `🔄 Reloaded ${ensembles.length} ensembles from portfolio` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown element type '${type}'` }] }; } } catch (error) { logger.error(`Failed to reload ${type}:`, error); return { content: [{ type: "text", text: `❌ Failed to reload ${type}: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Render a template with variables * Extracted from index.ts:682-701 (exact copy) * * @throws {ElementNotFoundError} When template does not exist * @see Issue #275 - Handlers return success=true for missing elements */ async renderTemplate(name, variables) { // Use the new TemplateRenderer utility for cleaner code and better validation const result = await this.templateRenderer.render(name, variables); if (result.success && result.content) { return { content: [{ type: "text", text: `📄 Rendered template '${name}':\n\n${result.content}` }] }; } else { // Issue #275: Throw error for missing templates instead of returning content if (result.error?.includes('not found')) { throw new ElementNotFoundError('Template', name); } return { content: [{ type: "text", text: `❌ ${result.error || 'Failed to render template'}` }] }; } } /** * Execute an agent with goal parameters * Returns context for LLM to drive the agentic loop */ async executeAgent(name, parameters) { try { const result = await this.agentManager.executeAgent(name, parameters); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { // FIX: Issue #275 - Re-throw ElementNotFoundError for consistent error handling if (error instanceof ElementNotFoundError) { throw error; } logger.error(`Failed to execute agent '${name}':`, error); return { content: [{ type: "text", text: `❌ Failed to execute agent: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Record a step in agent execution */ async recordAgentStep(args) { try { const result = await this.agentManager.recordAgentStep(args); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { // FIX: Issue #275 - Re-throw ElementNotFoundError for consistent error handling if (error instanceof ElementNotFoundError) { throw error; } logger.error(`Failed to record agent step for '${args.agentName}':`, error); return { content: [{ type: "text", text: `❌ Failed to record step: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Complete an agent goal */ async completeAgentGoal(args) { try { const result = await this.agentManager.completeAgentGoal(args); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { // FIX: Issue #275 - Re-throw ElementNotFoundError for consistent error handling if (error instanceof ElementNotFoundError) { throw error; } logger.error(`Failed to complete agent goal for '${args.agentName}':`, error); return { content: [{ type: "text", text: `❌ Failed to complete goal: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Get agent state */ async getAgentState(args) { try { const result = await this.agentManager.getAgentState(args); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { // FIX: Issue #275 - Re-throw ElementNotFoundError for consistent error handling if (error instanceof ElementNotFoundError) { throw error; } logger.error(`Failed to get agent state for '${args.agentName}':`, error); return { content: [{ type: "text", text: `❌ Failed to get state: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Continue agent execution from previous state */ async continueAgentExecution(args) { try { const result = await this.agentManager.continueAgentExecution(args); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { // FIX: Issue #275 - Re-throw ElementNotFoundError for consistent error handling if (error instanceof ElementNotFoundError) { throw error; } logger.error(`Failed to continue agent execution for '${args.agentName}':`, error); return { content: [{ type: "text", text: `❌ Failed to continue execution: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } /** * Aggregate active elements across all registered element types. * Issue #501: Called when get_active_elements is invoked without element_type. */ async aggregateActiveElements() { const sections = []; for (const [elementType, strategy] of this.strategies) { try { const result = await strategy.getActiveElements(); const text = result.content[0]?.text; if (text) { sections.push(`[${elementType}]\n${text}`); } } catch (err) { logger.debug(`Failed to get active ${elementType} elements`, { error: err }); } } return { content: [{ type: "text", text: sections.length > 0 ? sections.join('\n\n') : 'No active elements found.' }] }; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRWxlbWVudENSVURIYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2hhbmRsZXJzL0VsZW1lbnRDUlVESGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQkc7QUFFSCxPQUFPLEVBQUUsV0FBVyxFQUFvQixNQUFNLGtDQUFrQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUN6QixPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFPN0IsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxhQUFhLElBQUksb0JBQW9CLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUN4RixPQUFPLEVBQUUsYUFBYSxJQUFJLG9CQUFvQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDeEYsT0FBTyxFQUFFLFdBQVcsSUFBSSxrQkFBa0IsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ2xGLE9BQU8sRUFBRSxjQUFjLElBQUkscUJBQXFCLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUMzRixPQUFPLEVBQUUsWUFBWSxJQUFJLG1CQUFtQixFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDckYsT0FBTyxFQUFFLG1CQUFtQixFQUFFLGdCQUFnQixJQUFJLHNCQUFzQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDNUcsT0FBTyxFQUFFLGVBQWUsSUFBSSxzQkFBc0IsRUFBRSxNQUFNLG1DQUFtQyxDQUFDO0FBSzlGLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNqRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUd4RSxPQUFPLEVBQ0wsMEJBQTBCLEVBQzFCLHVCQUF1QixFQUN2Qix1QkFBdUIsRUFDdkIs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