UNPKG

me-engine-one

Version:

The magic file system that regenerates entire AI universes from me.md

321 lines (280 loc) 8.92 kB
import fs from 'fs-extra'; import path from 'path'; import matter from 'gray-matter'; import yaml from 'yaml'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { MeSchema } from '../schemas/me-schema.js'; /** * Parses and validates the magical me.md file * This is the heart of the single-source system */ export class MeParser { constructor() { this.ajv = new Ajv({ allErrors: true, verbose: true }); addFormats(this.ajv); this.validator = this.ajv.compile(MeSchema); this.cache = new Map(); } /** * Parse me.md file with full validation and error recovery * @param {string} filePath - Path to me.md file * @returns {Object} Parsed and validated configuration */ async parseMeFile(filePath) { try { // Check cache first const stats = await fs.stat(filePath); const cacheKey = `${filePath}:${stats.mtime.getTime()}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // Read and parse file const content = await fs.readFile(filePath, 'utf8'); const parsed = matter(content); // Parse YAML frontmatter let config; if (parsed.data && Object.keys(parsed.data).length > 0) { config = parsed.data; } else { // Try to parse content as YAML if no frontmatter try { config = yaml.parse(parsed.content); } catch (yamlError) { throw new Error(`Failed to parse me.md: No valid YAML frontmatter or content found`); } } // Validate against schema const isValid = this.validator(config); if (!isValid) { const errors = this.formatValidationErrors(this.validator.errors); throw new ValidationError('Invalid me.md configuration', errors); } // Apply defaults and enhancements const enriched = await this.enrichConfiguration(config); // Cache the result this.cache.set(cacheKey, enriched); return enriched; } catch (error) { if (error instanceof ValidationError) { throw error; } // Wrap other errors with helpful context throw new Error(`Failed to parse me.md: ${error.message}`); } } /** * Enrich configuration with computed values and defaults * @param {Object} config - Raw configuration * @returns {Object} Enriched configuration */ async enrichConfiguration(config) { const enriched = { ...config }; // Apply intelligent defaults enriched.generated = { timestamp: new Date().toISOString(), version: '1.0.0', parser_version: '1.0.0' }; // Ensure brand defaults if (!enriched.brand) enriched.brand = {}; enriched.brand = { theme: 'professional', colors: ['#2563EB', '#64748B', '#FFFFFF'], voice: 'Professional, helpful, engaging', font: 'Inter', cursor: 'default', ...enriched.brand }; // Validate and fix color formats if (enriched.brand.colors) { enriched.brand.colors = enriched.brand.colors.map(color => this.normalizeColor(color)); } // Ensure top_agents structure if (!enriched.top_agents) { enriched.top_agents = this.generateDefaultAgents(enriched.brand?.voice || 'professional'); } // Ensure required agents have slot assignments enriched.top_agents = this.validateAgentSlots(enriched.top_agents); // Ensure spaces structure if (!enriched.spaces) { enriched.spaces = [{ id: 'default', title: `${enriched.name || 'My'} AI Universe`, public: false, features: ['agents', 'missions'] }]; } // Ensure goals array if (!enriched.goals) { enriched.goals = ['Get started with AI collaboration']; } // Ensure style preferences if (!enriched.style) { enriched.style = {}; } enriched.style = { animations: 'subtle', widgets: ['top_8_agents'], profile_song: false, visitor_counter: false, guestbook: false, ...enriched.style }; return enriched; } /** * Generate default agents based on context * @param {string} voice - User's voice/personality * @returns {Array} Default agent configuration */ generateDefaultAgents(voice) { const isStudent = voice.toLowerCase().includes('student') || voice.toLowerCase().includes('learn'); const isProfessional = voice.toLowerCase().includes('business') || voice.toLowerCase().includes('professional'); if (isStudent) { return [ { slot: 1, name: 'ARIA', base: 'study-coordinator', role: 'Study Coordinator & Academic Advisor', personality: 'Encouraging academic companion', catchphrase: 'Let\'s make learning fun and effective!' }, { slot: 2, name: 'SAGE', base: 'homework-helper', role: 'Subject Matter Expert', personality: 'Patient teacher who explains clearly', catchphrase: 'Every question leads to understanding' } ]; } // Default professional agents return [ { slot: 1, name: 'ARIA', base: 'personal-assistant', role: 'Digital Twin & Chief of Staff', personality: 'Your voice, optimized for productivity', catchphrase: 'I am you, but optimized' }, { slot: 2, name: 'MUSE', base: 'creative-genius', role: 'Creative Genius & Content Creator', personality: 'Unhinged creativity meets strategic thinking', catchphrase: 'Let\'s create something extraordinary' } ]; } /** * Validate and fix agent slot assignments * @param {Array} agents - Agent configurations * @returns {Array} Fixed agent configurations */ validateAgentSlots(agents) { const fixed = [...agents]; // Ensure slot 1 exists and is ARIA const slot1 = fixed.find(a => a.slot === 1); if (!slot1) { fixed.unshift({ slot: 1, name: 'ARIA', base: 'personal-assistant', role: 'Personal Assistant', personality: 'Your digital coordinator', catchphrase: 'Ready to help!' }); } else if (slot1.name !== 'ARIA') { slot1.name = 'ARIA'; slot1.role = slot1.role || 'Personal Assistant'; } // Sort by slot and ensure consecutive numbering fixed.sort((a, b) => (a.slot || 999) - (b.slot || 999)); // Fix any missing slots for (let i = 0; i < fixed.length; i++) { if (!fixed[i].slot) { fixed[i].slot = i + 1; } } return fixed.slice(0, 8); // Limit to Top 8 } /** * Normalize color format (ensure hex codes) * @param {string} color - Color value * @returns {string} Normalized hex color */ normalizeColor(color) { if (!color) return '#000000'; // Already hex if (color.startsWith('#')) { return color.length === 4 ? color + color.slice(1) : // #RGB -> #RRGGBB color; } // Named colors to hex const namedColors = { red: '#FF0000', green: '#00FF00', blue: '#0000FF', white: '#FFFFFF', black: '#000000', yellow: '#FFFF00', cyan: '#00FFFF', magenta: '#FF00FF' }; return namedColors[color.toLowerCase()] || '#000000'; } /** * Format validation errors for user-friendly display * @param {Array} errors - AJV validation errors * @returns {Array} Formatted error messages */ formatValidationErrors(errors) { return errors.map(error => { const path = error.instancePath || 'root'; const message = error.message; const value = error.data; // Create helpful suggestions let suggestion = ''; if (error.keyword === 'format' && error.params?.format === 'color') { suggestion = 'Use hex format like #FF0000 or color names like "red"'; } else if (error.keyword === 'enum') { suggestion = `Available options: ${error.params.allowedValues.join(', ')}`; } else if (error.keyword === 'required') { suggestion = `Add the required field: ${error.params.missingProperty}`; } else if (error.keyword === 'type') { suggestion = `Expected ${error.params.type}, got ${typeof value}`; } return { path, message, value, suggestion, severity: error.keyword === 'required' ? 'error' : 'warning' }; }); } /** * Clear parser cache */ clearCache() { this.cache.clear(); } } /** * Custom validation error class */ export class ValidationError extends Error { constructor(message, errors = []) { super(message); this.name = 'ValidationError'; this.errors = errors; } } // Export singleton instance export const meParser = new MeParser();