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.

839 lines 132 kB
import { ElementType } from '../../portfolio/PortfolioManager.js'; import { Ensemble } from '../../elements/ensembles/Ensemble.js'; import { resolveElementTypes } from '../../utils/elementTypeResolver.js'; import { generateMemoryId } from '../../elements/memories/utils.js'; import { logger } from '../../utils/logger.js'; import { normalizeVersion } from '../../elements/BaseElement.js'; import { ValidationService } from '../../services/validation/ValidationService.js'; import { SECURITY_LIMITS } from '../../security/constants.js'; import { ContentValidator } from '../../security/contentValidator.js'; import { ElementNotFoundError } from '../../utils/ErrorHandler.js'; import { deepMerge, DANGEROUS_PROPERTIES } from '../../utils/deepMerge.js'; import { normalizeElementTypeInput, formatValidElementTypesList, getElementFilename, getElementTypeLabel, resolveElementByName, KNOWN_METADATA_PROPERTIES, detectUnknownMetadataProperties, formatUnknownPropertyWarnings, formatElementResolutionWarnings, collectGatekeeperAuthoringErrors, formatGatekeeperValidationMessage, } from './helpers.js'; // Read-only fields that cannot be modified through the edit API const READ_ONLY_FIELDS = new Set([ 'id', 'type', 'filePath', 'filename', '_status', '_isDirty' ]); function getMaxLengthForFieldType(fieldType) { switch (fieldType) { case 'name': return SECURITY_LIMITS.MAX_NAME_LENGTH; case 'description': return SECURITY_LIMITS.MAX_DESCRIPTION_LENGTH; case 'content': return SECURITY_LIMITS.MAX_CONTENT_LENGTH; case 'filename': return SECURITY_LIMITS.MAX_COMMAND_ARG_LENGTH; } } /** * Issue #662: Systematic field type validation at the editElement boundary. * * Defines expected types for known metadata fields across all element types. * Prevents silent data corruption when LLMs send wrong types (e.g., dict instead of array). * * Key: field name, Value: { expected: type description, check: validator function } */ const FIELD_TYPE_RULES = { // Array fields (across multiple types) tags: { expected: 'array of strings', check: (v) => Array.isArray(v) }, triggers: { expected: 'array of strings', check: (v) => Array.isArray(v) }, domains: { expected: 'array of strings', check: (v) => Array.isArray(v) }, examples: { expected: 'array', check: (v) => Array.isArray(v) }, prerequisites: { expected: 'array of strings', check: (v) => Array.isArray(v) }, capabilities: { expected: 'array', check: (v) => Array.isArray(v) }, goals: { expected: 'array', check: (v) => Array.isArray(v) }, constraints: { expected: 'array', check: (v) => Array.isArray(v) }, actions: { expected: 'array', check: (v) => Array.isArray(v) }, skills: { expected: 'array', check: (v) => Array.isArray(v) }, // Issue #724: activates and tools are objects for agents (not arrays) activates: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.AGENT] }, tools: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.AGENT] }, variables: { expected: 'array', check: (v) => Array.isArray(v), elementTypes: [ElementType.TEMPLATE] }, entries: { expected: 'array', check: (v) => Array.isArray(v), elementTypes: [ElementType.MEMORY] }, // elements handled separately by validateAndNormalizeEnsembleElements (accepts dict too) // Object fields goal: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.AGENT] }, autonomy: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.AGENT] }, resilience: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.AGENT] }, gatekeeper: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v) }, retentionPolicy: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.MEMORY] }, retention_policy: { expected: 'object', check: (v) => typeof v === 'object' && v !== null && !Array.isArray(v), elementTypes: [ElementType.MEMORY] }, // String fields // Issue #724: systemPrompt was missing from field type rules. // Both camelCase and snake_case variants accepted because LLMs frequently // pass snake_case keys; normalization to camelCase happens downstream in // AgentManager.parseMetadata() (Issue #722). systemPrompt: { expected: 'string', check: (v) => typeof v === 'string', elementTypes: [ElementType.AGENT] }, system_prompt: { expected: 'string', check: (v) => typeof v === 'string', elementTypes: [ElementType.AGENT] }, name: { expected: 'string', check: (v) => typeof v === 'string' }, description: { expected: 'string', check: (v) => typeof v === 'string' }, author: { expected: 'string', check: (v) => typeof v === 'string' }, version: { expected: 'string', check: (v) => typeof v === 'string' || typeof v === 'number' }, category: { expected: 'string', check: (v) => typeof v === 'string' }, domain: { expected: 'string', check: (v) => typeof v === 'string' }, tone: { expected: 'string', check: (v) => typeof v === 'string' }, // Number fields maxEntries: { expected: 'number', check: (v) => typeof v === 'number', elementTypes: [ElementType.MEMORY] }, max_entries: { expected: 'number', check: (v) => typeof v === 'number', elementTypes: [ElementType.MEMORY] }, priority: { expected: 'number', check: (v) => typeof v === 'number' }, }; /** * Issue #662: Validate field types in input before applying to element. * * Checks each field in the input against `FIELD_TYPE_RULES`. Fields not in the * rules map are skipped (they'll be caught by unknown-property detection later). * Element-type-scoped rules only apply when the target type matches. * * @param input - The edit input object containing fields to validate * @param elementType - The normalized element type being edited * @returns Array of error messages for fields with wrong types (empty if all valid) */ function validateFieldTypes(input, elementType) { const errors = []; for (const [field, value] of Object.entries(input)) { if (value === undefined || value === null) continue; const rule = FIELD_TYPE_RULES[field]; if (!rule) continue; // Skip if rule doesn't apply to this element type if (rule.elementTypes && !rule.elementTypes.includes(elementType)) continue; if (!rule.check(value)) { errors.push(`Field '${field}' expects ${rule.expected}, got ${Array.isArray(value) ? 'array' : typeof value}`); } } return errors; } // Merge options for secure deep merging const MERGE_OPTIONS = { skipProperties: DANGEROUS_PROPERTIES, readOnlyFields: READ_ONLY_FIELDS }; /** * Map field names to their appropriate validation field types. * This ensures consistent validation between edit (input) and load (output). */ function getFieldTypeForValidation(fieldName) { // Normalize field name (handle both 'description' and 'metadata.description') const normalizedField = fieldName.replace(/^metadata\./, ''); switch (normalizedField) { case 'name': return 'name'; case 'description': return 'description'; case 'content': return 'content'; default: return null; // No special validation needed } } /** * Validate a field value before setting it on an element. * Returns null if valid, or an error message if invalid. * * @param validationService - Injected validation service for testability * @param field - The field name being validated * @param value - The value to validate */ function validateFieldValue(validationService, field, value) { // Only validate string values for text fields if (typeof value !== 'string') { return null; // Non-string values (arrays, objects, booleans, numbers) skip text validation } const fieldType = getFieldTypeForValidation(field); if (!fieldType) { return null; // No validation rules for this field } const maxLength = getMaxLengthForFieldType(fieldType); const result = validationService.validateAndSanitizeInput(value, { maxLength, allowSpaces: true, fieldType }); if (!result.isValid) { return result.errors?.join(', ') || 'Validation failed'; } return null; // Valid } function getTraversalEntries(value, path) { if (Array.isArray(value)) { return value.map((item, index) => [`${path}[${index}]`, item]); } return Object.entries(value).map(([key, item]) => [`${path}.${key}`, item]); } function getDescriptionMaxLengthForPath(fieldPath) { if (fieldPath === 'input.description' || fieldPath === 'input.metadata.description') { return SECURITY_LIMITS.MAX_DESCRIPTION_LENGTH; } return SECURITY_LIMITS.MAX_DOCUMENTATION_FIELD_LENGTH; } function isOversizedDescriptionField(fieldPath, value) { const maxLength = getDescriptionMaxLengthForPath(fieldPath); return (fieldPath.endsWith('.description') && typeof value === 'string' && value.length > maxLength); } function formatOversizedDescriptionField(field) { return (` • ${field.path} exceeds maximum length of ` + `${field.maxLength} characters (${field.length})`); } function findOversizedDescriptionFields(value, path = 'input', seen = new WeakSet()) { if (value === null || typeof value !== 'object') { return []; } if (seen.has(value)) { return []; } seen.add(value); const oversized = []; for (const [fieldPath, item] of getTraversalEntries(value, path)) { if (isOversizedDescriptionField(fieldPath, item)) { oversized.push({ path: fieldPath, length: item.length, maxLength: getDescriptionMaxLengthForPath(fieldPath), }); } oversized.push(...findOversizedDescriptionFields(item, fieldPath, seen)); } return oversized; } /** * Validate and normalize ensemble elements input. * * Issue #658: LLMs often send elements in dict-keyed format instead of array format. * This function normalizes both formats to a valid array of EnsembleElement-like objects. * * Accepted formats: * - Array: [{ element_name: "foo", element_type: "memory", ... }] * - Dict: { "foo": { type: "memory", ... } } → converted to array * * @param value - The elements input to validate * @returns Object with success flag, normalized elements array, and optional error */ function validateAndNormalizeEnsembleElements(value) { // Case 1: Already an array — validate each item if (Array.isArray(value)) { const errors = []; const normalized = []; for (let i = 0; i < value.length; i++) { const item = value[i]; if (!item || typeof item !== 'object' || Array.isArray(item)) { errors.push(`Element at index ${i} must be an object with element_name and element_type`); continue; } const obj = item; const name = obj.element_name || obj.name; const type = obj.element_type || obj.type; if (!name) { errors.push(`Element at index ${i} is missing element_name (or name)`); continue; } // Validate _remove is strictly boolean if present if ('_remove' in obj && typeof obj._remove !== 'boolean') { errors.push(`Element '${String(name)}' has invalid _remove value — must be boolean true, got ${typeof obj._remove}`); continue; } // Normalize to standard field names, only including defined values const entry = { ...obj, element_name: String(name) }; if (type) { entry.element_type = String(type); } normalized.push(entry); } if (errors.length > 0) { return { success: false, elements: [], error: errors.join('; ') }; } return { success: true, elements: normalized }; } // Case 2: Dict-keyed format — convert to array if (value !== null && typeof value === 'object' && !Array.isArray(value)) { const dict = value; const normalized = []; for (const [key, val] of Object.entries(dict)) { if (!val || typeof val !== 'object' || Array.isArray(val)) { return { success: false, elements: [], error: `Element '${key}' in dict format must be an object with at least element_type (or type). ` + `Expected: { "${key}": { type: "memory", role: "support", ... } }` }; } const inner = val; normalized.push({ ...inner, element_name: String(inner.element_name || inner.name || key), element_type: String(inner.element_type || inner.type), role: inner.role || 'support', activation: inner.activation || 'always', priority: inner.priority ?? 50, }); } return { success: true, elements: normalized }; } // Case 3: Invalid type return { success: false, elements: [], error: `'elements' must be an array of objects or a dict-keyed object. Got: ${typeof value}` }; } /** * Merge incoming ensemble elements with existing elements. * * Issue #658/#662: Unified collection editing semantics — each item in the input * declares its own intent via its properties: * * - Existing element with updates → merge properties (incoming wins) * - New element (not found by name) → appended * - Element with `_remove: true` → dropped from collection * - Existing elements not in incoming → preserved unchanged * * This pattern extends to any future grouping type (workflows, pipelines, teams). * * @param existing - Current elements array from the ensemble * @param incoming - New/updated/removed elements from the edit input * @returns Object with merged array and any warnings */ function mergeEnsembleElements(existing, incoming) { const result = existing.map(e => ({ ...e })); const warnings = []; for (const inc of incoming) { const name = String(inc.element_name || inc.name || ''); if (!name) continue; const existingIndex = result.findIndex(e => String(e.element_name || e.name || '') === name); // Issue #662: _remove marker — drop element from collection if (inc._remove === true) { if (existingIndex >= 0) { result.splice(existingIndex, 1); } else { warnings.push(`Element '${name}' not found in ensemble — nothing to remove`); } continue; } // Strip transient/alias fields that shouldn't be persisted const { _remove: _discardRemove, name: _discardName, type: _discardType, ...persistable } = inc; if (existingIndex >= 0) { // Upsert: merge incoming properties into existing (incoming wins) result[existingIndex] = { ...result[existingIndex], ...persistable }; } else { // Append new element result.push({ ...persistable }); } } return { elements: result, warnings }; } /** * Validate, normalize, and merge ensemble elements into the update object. * Extracted to avoid duplication between the top-level `elements` key handler * and the `metadata.elements` key handler (Claude review). * * @returns An error string if validation fails, or null on success */ function isEnsembleElementInput(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function applyEnsembleElementsUpdate(elementsInput, element, updateObj, collectionWarnings) { const validated = validateAndNormalizeEnsembleElements(elementsInput); if (!validated.success) { return `Invalid 'elements' format: ${validated.error}`; } const elementRecord = element; const metadata = elementRecord.metadata; const rawElements = metadata?.elements; const existingTyped = Array.isArray(rawElements) ? rawElements.filter(isEnsembleElementInput).map(e => ({ ...e })) : []; const mergeResult = mergeEnsembleElements(existingTyped, validated.elements); collectionWarnings.push(...mergeResult.warnings); if (!updateObj.metadata) { updateObj.metadata = {}; } updateObj.metadata.elements = mergeResult.elements; return null; } /** * Sync ensemble elements from metadata after an update. * * Ensembles store their element references in metadata.elements, * and need to sync the internal elements array when that changes. * * Issue #466: Before syncing, resolve any missing element_type fields * by searching the portfolio via the context managers. * * @param element - The ensemble element to sync * @param input - The input object to check for element updates * @param context - Element CRUD context with managers for type resolution */ async function syncEnsembleElementsIfNeeded(element, input, context) { const hasElementsUpdate = input.elements || input.metadata?.elements; if (hasElementsUpdate && element instanceof Ensemble) { // Issue #466: Resolve missing element_type via portfolio lookup before sync if (context && element.metadata.elements) { const result = await resolveElementTypes(element.metadata.elements, { skillManager: context.skillManager, templateManager: context.templateManager, agentManager: context.agentManager, memoryManager: context.memoryManager, personaManager: context.personaManager, ensembleManager: context.ensembleManager, }); element.metadata.elements = result.resolved; element.syncElementsFromMetadata(); return result; } element.syncElementsFromMetadata(); } return undefined; } /** * Handle memory entry updates with validation and normalization. * * Memory entries require special handling because they: * - Need ID generation if missing * - Require timestamp parsing and validation * - Use a Map structure internally * * @param entries - Array of memory entries from input * @returns Result with success status, optional error message, and normalized entries map */ function handleMemoryEntryUpdate(entries) { const entriesMap = new Map(); const errors = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (!entry || typeof entry !== 'object') { errors.push(`Entry at index ${i} is not a valid object`); continue; } const entryObj = entry; const entryId = entryObj.id; if (!entryId) { entryObj.id = generateMemoryId(); } try { const timestamp = entryObj.timestamp ? new Date(entryObj.timestamp) : new Date(); if (Number.isNaN(timestamp.getTime())) { throw new Error('Invalid timestamp'); } const normalizedEntry = { ...entryObj, timestamp, tags: Array.isArray(entryObj.tags) ? entryObj.tags : [], metadata: typeof entryObj.metadata === 'object' && entryObj.metadata !== null ? entryObj.metadata : {} }; entriesMap.set(entryObj.id, normalizedEntry); } catch { errors.push(`Entry '${entryObj.id}' has invalid timestamp`); } } if (errors.length > 0) { return { success: false, message: errors.join('\n'), entriesMap }; } return { success: true, entriesMap }; } /** * Edit an existing element using GraphQL-aligned nested input. * * @param context - Element CRUD context with managers * @param args - Edit arguments (name, type, input) * @param validationService - Optional validation service for DI (creates default if not provided) */ export async function editElement(context, args, validationService) { await context.ensureInitialized(); // Use injected validation service or create default const validator = validationService ?? new ValidationService(); const { name, type, input } = args; // Validate input is provided if (!input || typeof input !== 'object') { return error('Missing or invalid input object. Provide fields to update as a nested object.'); } const { type: normalizedType } = normalizeElementTypeInput(type); if (!normalizedType) { return error(`Invalid element type '${type}'. Valid types: ${formatValidElementTypesList()}`); } const manager = getManagerForType(context, normalizedType); if (!manager) { const labelPlural = getElementTypeLabel(normalizedType, { plural: true }); return error(`Element type '${labelPlural}' is not yet supported for editing`); } const element = await resolveElementByName(manager, normalizedType, name); if (!element) { const label = getElementTypeLabel(normalizedType); throw new ElementNotFoundError(label, name); } // Check for unknown properties and generate warnings const unknownPropertyWarnings = detectUnknownMetadataProperties(normalizedType, input); // Also validate nested metadata sub-object (the top-level 'metadata' key is // skipped by detectUnknownMetadataProperties as a special route field, so // dangerous/unknown properties inside it would go unchecked otherwise) if (input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)) { const nestedWarnings = detectUnknownMetadataProperties(normalizedType, input.metadata); unknownPropertyWarnings.push(...nestedWarnings); } const warningText = formatUnknownPropertyWarnings(unknownPropertyWarnings); if (unknownPropertyWarnings.length > 0) { logger.warn(`[editElement] Unknown properties detected`, { elementType: normalizedType, elementName: name, warningCount: unknownPropertyWarnings.length, unknownProperties: unknownPropertyWarnings.map(w => ({ property: w.property, suggestion: w.suggestion })) }); } // Issue #1591: Validate element-specific constraints before attempting edits // Memory content is read-only (append-only architecture) - reject attempts to edit it if (normalizedType === ElementType.MEMORY && 'content' in input) { return error(`Memory content cannot be modified via edit_element. ` + `Memory uses an append-only architecture. ` + `Use the 'addEntry' operation to add new entries to this memory.`); } // Issue #662: Validate field types before applying (prevents silent data corruption) const typeErrors = validateFieldTypes(input, normalizedType); if (typeErrors.length > 0) { return error(`Field type validation failed:\n${typeErrors.map(e => ` • ${e}`).join('\n')}`); } const gatekeeperErrors = collectGatekeeperAuthoringErrors(input, input.metadata); if (gatekeeperErrors.length > 0) { return error(formatGatekeeperValidationMessage(gatekeeperErrors)); } const oversizedDescriptionFields = findOversizedDescriptionFields(input); if (oversizedDescriptionFields.length > 0) { return error(`Description length validation failed:\n${oversizedDescriptionFields.map(field => formatOversizedDescriptionField(field)).join('\n')}`); } // Validate string field values using injected validator for (const [field, value] of Object.entries(input)) { if (typeof value === 'string') { const validationError = validateFieldValue(validator, field, value); if (validationError) { return error(`Invalid value for '${field}': ${validationError}`); } } } // Build the update object, mapping top-level fields to metadata where appropriate // Issue #565: Use type-specific metadata properties from authoritative source const metadataFields = KNOWN_METADATA_PROPERTIES[normalizedType] ?? new Set(); const updateObj = {}; const unrecognizedFieldWarnings = []; const collectionWarnings = []; const appliedFields = []; const skippedFields = []; // Fields handled by dedicated branches (not metadata routing) const specialRouteFields = new Set(['instructions', 'content', 'elements', 'metadata', 'entries', 'version']); // Content context map shared by instructions and content validation branches const contentContextMap = { [ElementType.AGENT]: 'agent', [ElementType.SKILL]: 'skill', [ElementType.TEMPLATE]: 'template', [ElementType.PERSONA]: 'persona', [ElementType.MEMORY]: 'memory', }; for (const [key, value] of Object.entries(input)) { // Skip dangerous properties if (DANGEROUS_PROPERTIES.includes(key)) { skippedFields.push(key); continue; } // Skip read-only fields if (READ_ONLY_FIELDS.has(key)) { logger.warn(`[editElement] Skipping read-only field: ${key}`); skippedFields.push(key); continue; } // Every branch below applies the field — track once at the end let fieldApplied = true; // Map known metadata fields automatically (type-specific) if (metadataFields.has(key) && !specialRouteFields.has(key)) { if (!updateObj.metadata) { updateObj.metadata = {}; } updateObj.metadata[key] = value; } else if (key === 'elements' && normalizedType === ElementType.ENSEMBLE) { // Issue #658: Validate and normalize elements input (array or dict format) const elemError = applyEnsembleElementsUpdate(value, element, updateObj, collectionWarnings); if (elemError) return error(elemError); } else if (key === 'metadata' && typeof value === 'object' && value !== null) { const metaValue = value; // If metadata.elements is provided for an ensemble, extract and route through // the ensemble elements handler for proper validation/normalization/merge. if (normalizedType === ElementType.ENSEMBLE && metaValue.elements) { const elemError = applyEnsembleElementsUpdate(metaValue.elements, element, updateObj, collectionWarnings); if (elemError) return error(elemError); // Remove elements from metadata value before deep merge to avoid double-processing const { elements: _extracted, ...restMetadata } = metaValue; if (Object.keys(restMetadata).length > 0) { updateObj.metadata = deepMerge(updateObj.metadata, restMetadata, MERGE_OPTIONS); } } else { // Merge nested metadata with security options updateObj.metadata = deepMerge((updateObj.metadata || {}), metaValue, MERGE_OPTIONS); } } else if (key === 'instructions' && typeof value === 'string') { // Issue #602 resolved: 'instructions' is a first-class field (behavioral directives) const contentValidation = ContentValidator.validateAndSanitize(value, { maxLength: SECURITY_LIMITS.MAX_CONTENT_LENGTH, contentContext: contentContextMap[normalizedType] }); const sanitizedInstructions = contentValidation.sanitizedContent || ''; // Set instructions on the element directly (all types now have this property) element.instructions = sanitizedInstructions; // For agents, also update extensions.instructions for backward compat if (normalizedType === ElementType.AGENT) { if (!element.extensions) { element.extensions = {}; } element.extensions.instructions = sanitizedInstructions; } } else if (key === 'content' && typeof value === 'string') { // Issue #585/#602: Handle content updates (reference material) const contentValidation = ContentValidator.validateAndSanitize(value, { maxLength: SECURITY_LIMITS.MAX_CONTENT_LENGTH, contentContext: contentContextMap[normalizedType] }); const sanitizedContent = contentValidation.sanitizedContent || ''; // Set content on the element directly (all types now have this property) element.content = sanitizedContent; } else if (specialRouteFields.has(key)) { // Special route field — handled elsewhere, skip fieldApplied = false; } else { // Issue #565: Field not in type-specific metadata set and not a special route — // still route to updateObj for backward compat, but warn that it may not persist updateObj[key] = value; unrecognizedFieldWarnings.push(`Field '${key}' is not a recognized metadata property for ${getElementTypeLabel(normalizedType)} — it may not persist after save.`); logger.warn(`[editElement] Unrecognized field '${key}' for ${normalizedType}, routing to element object`, { elementType: normalizedType, elementName: name, field: key, }); } if (fieldApplied) { appliedFields.push(key); } } // Deep merge the update into the element with security options const elementData = element; const mergedData = deepMerge(elementData, updateObj, MERGE_OPTIONS); // Apply merged data back to element for (const [key, value] of Object.entries(mergedData)) { if (key !== 'metadata') { element[key] = value; } } // Handle metadata separately to preserve element structure if (mergedData.metadata && element.metadata) { Object.assign(element.metadata, mergedData.metadata); } // Handle memory entries specially (extracted for clarity) if (normalizedType === ElementType.MEMORY && input.entries && Array.isArray(input.entries)) { const memoryResult = handleMemoryEntryUpdate(input.entries); if (!memoryResult.success) { return error(`Memory entry validation errors:\n${memoryResult.message}\n\nValid entries were saved.`); } element.entries = memoryResult.entriesMap; } // Sync ensemble elements from metadata (extracted for clarity) // Issue #466: Resolve element_type via portfolio lookup before sync, surface warnings let resolutionWarningText = ''; if (normalizedType === ElementType.ENSEMBLE) { const resolutionResult = await syncEnsembleElementsIfNeeded(element, input, context); if (resolutionResult) { resolutionWarningText = formatElementResolutionWarnings(resolutionResult); } } // Fix #911: Normalize metadata after merge to ensure structural consistency. // BaseElement constructor runs normalizeMetadata on create/load, but the edit path // modifies metadata in-place. This ensures essential defaults survive edits. if (element.metadata) { element.metadata.description ??= ''; element.metadata.tags ??= []; // Update modified timestamp element.metadata.modified = new Date().toISOString(); } // Handle version updates if (input.version !== undefined) { const update = updateVersionExplicit(element, String(input.version)); if (!update.success) { return error(update.message || 'Failed to update version'); } } else { autoIncrementVersion(element); } // Determine file path for saving const filePathCandidate = typeof element.getFilePath === 'function' ? element.getFilePath() : (element.filePath || element.filename); const filename = typeof filePathCandidate === 'string' && filePathCandidate.length > 0 ? filePathCandidate : getElementFilename(normalizedType, element.metadata?.name || name); try { await manager.save(element, filename); } catch (err) { return error(`Failed to save element: ${err instanceof Error ? err.message : 'Unknown error'}`); } const label = getElementTypeLabel(normalizedType); const displayName = element.metadata?.name || name; const updatedFields = appliedFields.join(', '); // Issue #1656: If all fields were silently skipped, report honestly if (appliedFields.length === 0 && skippedFields.length > 0) { return { content: [{ type: 'text', text: `⚠️ No fields applied to ${label} '${displayName}'. ` + `Skipped: ${skippedFields.join(', ')} (dangerous or read-only fields are not editable).` }] }; } // Issue #565: Include warnings for unrecognized fields in response let unrecognizedWarningText = ''; if (unrecognizedFieldWarnings.length > 0) { const lines = ['⚠️ **Unrecognized Field Warnings:**']; for (const warning of unrecognizedFieldWarnings) { lines.push(` • ${warning}`); } lines.push(''); unrecognizedWarningText = lines.join('\n'); } // Issue #662: Include collection operation warnings let collectionWarningText = ''; if (collectionWarnings.length > 0) { const lines = ['⚠️ **Collection Warnings:**']; for (const warning of collectionWarnings) { lines.push(` • ${warning}`); } lines.push(''); collectionWarningText = lines.join('\n'); } // Issue #1656: Report skipped fields so LLMs don't assume they were applied const skippedText = skippedFields.length > 0 ? `\n⚠️ Skipped: ${skippedFields.join(', ')} (dangerous or read-only)` : ''; return { content: [{ type: 'text', text: `${warningText}${resolutionWarningText}${unrecognizedWarningText}${collectionWarningText}✅ Updated ${label} '${displayName}' - fields: ${updatedFields}${skippedText}` }] }; } function error(message) { return { content: [{ type: 'text', text: `❌ ${message}` }] }; } function getManagerForType(context, type) { switch (type) { case ElementType.PERSONA: return context.personaManager; case ElementType.SKILL: return context.skillManager; case ElementType.TEMPLATE: return context.templateManager; case ElementType.AGENT: return context.agentManager; case ElementType.MEMORY: return context.memoryManager; case ElementType.ENSEMBLE: return context.ensembleManager; default: return null; } } function updateVersionExplicit(element, versionString) { const isValid = /^(\d+)(\.\d+)?(\.\d+)?(-[a-zA-Z0-9.-]+)?$/.test(versionString); if (!isValid) { return { success: false, message: `Invalid version format: '${versionString}'. Please use format like 1.0.0, 1.0, or 1` }; } try { element.version = versionString; if (element.metadata) { element.metadata.version = versionString; } return { success: true }; } catch (error) { logger.error('Failed to update version', { error }); return { success: false, message: `Failed to update version: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } function autoIncrementVersion(element) { try { if (element.version) { // First normalize the version to 3-part format (handles legacy 2-part versions) const normalized = normalizeVersion(String(element.version)); // Check for pre-release versions const preReleaseMatch = normalized.match(/^(\d+\.\d+\.\d+)(-([a-zA-Z0-9.-]+))?$/); if (preReleaseMatch) { const baseVersion = preReleaseMatch[1]; const preReleaseTag = preReleaseMatch[3]; if (preReleaseTag) { // Increment pre-release version (e.g., "1.0.0-beta.1" → "1.0.0-beta.2") const preReleaseNumberMatch = preReleaseTag.match(/^([a-zA-Z]+)\.?(\d+)?$/); if (preReleaseNumberMatch) { const preReleaseType = preReleaseNumberMatch[1]; const preReleaseNumber = Number.parseInt(preReleaseNumberMatch[2] || '0') + 1; element.version = `${baseVersion}-${preReleaseType}.${preReleaseNumber}`; } else { // No pre-release number, bump patch const [major, minor, patch] = baseVersion.split('.').map(Number); element.version = `${major}.${minor}.${patch + 1}`; } } else { // Standard version, bump patch (e.g., "1.0.0" → "1.0.1") const [major, minor, patch] = baseVersion.split('.').map(Number); element.version = `${major}.${minor}.${patch + 1}`; } } else { // Shouldn't reach here after normalization, but fallback to safe default element.version = '1.0.1'; } } else { // No version, start at 1.0.0 element.version = '1.0.0'; } // Sync version to metadata if (element.metadata) { element.metadata.version = element.version; } } catch (error) { logger.error('Failed to auto-increment version', { error }); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWRpdEVsZW1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvaGFuZGxlcnMvZWxlbWVudC1jcnVkL2VkaXRFbGVtZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQUtsRSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sc0NBQXNDLENBQUM7QUFFaEUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFFekUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDcEUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQy9DLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxpQkFBaUIsRUFBNEIsTUFBTSxnREFBZ0QsQ0FBQztBQUM3RyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDOUQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDdEUsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDbkUsT0FBTyxFQUFFLFNBQVMsRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQzNFLE9BQU8sRUFDTCx5QkFBeUIsRUFDekIsMkJBQTJCLEVBQzNCLGtCQUFrQixFQUNsQixtQkFBbUIsRUFDbkIsb0JBQW9CLEVBRXBCLHlCQUF5QixFQUN6QiwrQkFBK0IsRUFDL0IsNkJBQTZCLEVBQzdCLCtCQUErQixFQUMvQixnQ0FBZ0MsRUFDaEMsaUNBQWlDLEdBQ2xDLE1BQU0sY0FBYyxDQUFDO0FBaUN0QixnRUFBZ0U7QUFDaEUsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLEdBQUcsQ0FBQztJQUMvQixJQUFJO0lBQ0osTUFBTTtJQUNOLFVBQVU7SUFDVixVQUFVO0lBQ1YsU0FBUztJQUNULFVBQVU7Q0FDWCxDQUFDLENBQUM7QUFFSCxTQUFTLHdCQUF3QixDQUFDLFNBQThCO0lBQzlELFFBQVEsU0FBUyxFQUFFLENBQUM7UUFDbEIsS0FBSyxNQUFNO1lBQ1QsT0FBTyxlQUFlLENBQUMsZUFBZSxDQUFDO1FBQ3pDLEtBQUssYUFBYTtZQUNoQixPQUFPLGVBQWUsQ0FBQyxzQkFBc0IsQ0FBQztRQUNoRCxLQUFLLFNBQVM7WUFDWixPQUFPLGVBQWUsQ0FBQyxrQkFBa0IsQ0FBQztRQUM1QyxLQUFLLFVBQVU7WUFDYixPQUFPLGVBQWUsQ0FBQyxzQkFBc0IsQ0FBQztJQUNsRCxDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLGdCQUFnQixHQUlqQjtJQUNILHVDQUF1QztJQUN2QyxJQUFJLEVBQUUsRUFBRSxRQUFRLEVBQUUsa0JBQWtCLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQ3RFLFFBQVEsRUFBRSxFQUFFLFFBQVEsRUFBRSxrQkFBa0IsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUU7SUFDMUUsT0FBTyxFQUFFLEVBQUUsUUFBUSxFQUFFLGtCQUFrQixFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRTtJQUN6RSxRQUFRLEVBQUUsRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRTtJQUMvRCxhQUFhLEVBQUUsRUFBRSxRQUFRLEVBQUUsa0JBQWtCLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQy9FLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQ25FLEtBQUssRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQzVELFdBQVcsRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQ2xFLE9BQU8sRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQzlELE1BQU0sRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO0lBQzdELHNFQUFzRTtJQUN0RSxTQUFTLEVBQUUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFLFlBQVksRUFBRSxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsRUFBRTtJQUM1SSxLQUFLLEVBQUUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxJ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