/** * Augmentation Discovery API * * Provides discovery and configuration capabilities for augmentations * Enables tools like brain-cloud to dynamically discover, configure, and manage augmentations */ import { AugmentationRegistry } from './brainyAugmentation.js' import { AugmentationManifest, JSONSchema } from './manifest.js' import { AugmentationConfigResolver } from './configResolver.js' /** * Augmentation listing with manifest and status */ export interface AugmentationListing { id: string name: string manifest: AugmentationManifest status: { enabled: boolean initialized: boolean category: string priority: number } config?: { current: any schema?: JSONSchema sources?: any[] } } /** * Configuration validation result */ export interface ConfigValidationResult { valid: boolean errors?: string[] warnings?: string[] suggestions?: string[] } /** * Discovery API options */ export interface DiscoveryOptions { includeConfig?: boolean includeSchema?: boolean includeSources?: boolean category?: string enabled?: boolean } /** * Augmentation Discovery API * * Provides a unified interface for discovering and managing augmentations */ export class AugmentationDiscovery { constructor(private registry: AugmentationRegistry) {} /** * Discover all registered augmentations with manifests * @param options Discovery options * @returns List of augmentation listings */ async discover(options: DiscoveryOptions = {}): Promise { const augmentations = this.registry.getAll() const listings: AugmentationListing[] = [] for (const aug of augmentations) { // Check if augmentation has manifest support const hasManifest = 'getManifest' in aug && typeof aug.getManifest === 'function' if (!hasManifest) { // Skip augmentations without manifest support (legacy) continue } try { // Check if augmentation has manifest method if (!('getManifest' in aug) || typeof aug.getManifest !== 'function') { continue } const getManifestFn = aug.getManifest as Function const manifest = getManifestFn() // Apply filters if (options.category && manifest.category !== options.category) { continue } if (options.enabled !== undefined) { const isEnabled = (aug as any).enabled !== false if (isEnabled !== options.enabled) { continue } } // Build listing const listing: AugmentationListing = { id: manifest.id, name: manifest.name, manifest, status: { enabled: (aug as any).enabled !== false, initialized: (aug as any).isInitialized || false, category: (aug as any).category || manifest.category, priority: aug.priority } } // Include configuration if requested if (options.includeConfig && 'getConfig' in aug) { const getConfigFn = aug.getConfig as Function listing.config = { current: getConfigFn() } if (options.includeSchema) { listing.config.schema = manifest.configSchema } } listings.push(listing) } catch (error) { console.warn(`Failed to get manifest for augmentation ${aug.name}:`, error) } } // Sort by priority (highest first) then by name listings.sort((a, b) => { const priorityDiff = b.status.priority - a.status.priority if (priorityDiff !== 0) return priorityDiff return a.name.localeCompare(b.name) }) return listings } /** * Get a specific augmentation's manifest * @param augId Augmentation ID * @returns Augmentation manifest or null if not found */ async getManifest(augId: string): Promise { const aug = this.registry.get(augId) if (!aug || !('getManifest' in aug)) { return null } try { const getManifestFn = aug.getManifest as Function return getManifestFn() } catch (error) { console.error(`Failed to get manifest for ${augId}:`, error) return null } } /** * Get configuration schema for an augmentation * @param augId Augmentation ID * @returns Configuration schema or null */ async getConfigSchema(augId: string): Promise { const manifest = await this.getManifest(augId) return manifest?.configSchema || null } /** * Get current configuration for an augmentation * @param augId Augmentation ID * @returns Current configuration or null */ async getConfig(augId: string): Promise { const aug = this.registry.get(augId) if (!aug || !('getConfig' in aug)) { return null } try { const getConfigFn = aug.getConfig as Function return getConfigFn() } catch (error) { console.error(`Failed to get config for ${augId}:`, error) return null } } /** * Update configuration for an augmentation * @param augId Augmentation ID * @param config New configuration * @returns Updated configuration or null on failure */ async updateConfig(augId: string, config: any): Promise { const aug = this.registry.get(augId) if (!aug || !('updateConfig' in aug) || !('getConfig' in aug)) { throw new Error(`Augmentation ${augId} does not support configuration updates`) } try { const updateConfigFn = aug.updateConfig as Function await updateConfigFn(config) const getConfigFn = aug.getConfig as Function return getConfigFn() } catch (error) { throw new Error(`Failed to update config for ${augId}: ${error}`) } } /** * Validate configuration against schema * @param augId Augmentation ID * @param config Configuration to validate * @returns Validation result */ async validateConfig(augId: string, config: any): Promise { const schema = await this.getConfigSchema(augId) if (!schema) { return { valid: true, warnings: ['No schema available for validation'] } } const errors: string[] = [] const warnings: string[] = [] const suggestions: string[] = [] // Check required fields if (schema.required) { for (const field of schema.required) { if (config[field] === undefined) { errors.push(`Missing required field: ${field}`) } } } // Validate properties if (schema.properties) { for (const [key, propSchema] of Object.entries(schema.properties)) { const value = config[key] if (value === undefined) { // Check if there's a default if (propSchema.default !== undefined) { suggestions.push(`Field '${key}' not provided, will use default: ${JSON.stringify(propSchema.default)}`) } continue } // Type validation if (propSchema.type) { const actualType = Array.isArray(value) ? 'array' : typeof value if (actualType !== propSchema.type) { errors.push(`${key}: expected ${propSchema.type}, got ${actualType}`) } } // Additional validations for specific types this.validatePropertyValue(key, value, propSchema, errors, warnings) } } // Check for unknown properties if (schema.additionalProperties === false && schema.properties) { const allowedKeys = Object.keys(schema.properties) for (const key of Object.keys(config)) { if (!allowedKeys.includes(key)) { warnings.push(`Unknown property: ${key}`) } } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined, warnings: warnings.length > 0 ? warnings : undefined, suggestions: suggestions.length > 0 ? suggestions : undefined } } /** * Validate a property value against its schema */ private validatePropertyValue( key: string, value: any, schema: JSONSchema, errors: string[], warnings: string[] ): void { // Number validations if (schema.type === 'number') { if (schema.minimum !== undefined && value < schema.minimum) { errors.push(`${key}: value ${value} is less than minimum ${schema.minimum}`) } if (schema.maximum !== undefined && value > schema.maximum) { errors.push(`${key}: value ${value} is greater than maximum ${schema.maximum}`) } } // String validations if (schema.type === 'string') { if (schema.minLength !== undefined && value.length < schema.minLength) { errors.push(`${key}: length ${value.length} is less than minimum ${schema.minLength}`) } if (schema.maxLength !== undefined && value.length > schema.maxLength) { errors.push(`${key}: length ${value.length} is greater than maximum ${schema.maxLength}`) } if (schema.pattern) { const regex = new RegExp(schema.pattern) if (!regex.test(value)) { errors.push(`${key}: value does not match pattern ${schema.pattern}`) } } } // Enum validation if (schema.enum && !schema.enum.includes(value)) { errors.push(`${key}: value '${value}' is not one of allowed values: ${schema.enum.join(', ')}`) } } /** * Get environment variables for an augmentation * @param augId Augmentation ID * @returns Map of environment variable names to descriptions */ async getEnvironmentVariables(augId: string): Promise | null> { const manifest = await this.getManifest(augId) if (!manifest?.configSchema?.properties) { return null } const prefix = `BRAINY_AUG_${augId.toUpperCase()}_` const vars: Record = {} for (const [key, prop] of Object.entries(manifest.configSchema.properties)) { const envKey = prefix + key.replace(/([A-Z])/g, '_$1').toUpperCase() vars[envKey] = { configKey: key, description: prop.description, type: prop.type, default: prop.default, required: manifest.configSchema.required?.includes(key), currentValue: typeof process !== 'undefined' ? process.env?.[envKey] : undefined } } return vars } /** * Get configuration examples for an augmentation * @param augId Augmentation ID * @returns Configuration examples or empty array */ async getConfigExamples(augId: string): Promise { const manifest = await this.getManifest(augId) return manifest?.configExamples || [] } /** * Check if an augmentation supports configuration * @param augId Augmentation ID * @returns True if augmentation supports configuration */ async supportsConfiguration(augId: string): Promise { const aug = this.registry.get(augId) return !!(aug && 'getConfig' in aug && 'updateConfig' in aug) } /** * Get augmentations by category * @param category Category to filter by * @returns List of augmentations in the category */ async getByCategory(category: string): Promise { return this.discover({ category }) } /** * Get enabled augmentations * @returns List of enabled augmentations */ async getEnabled(): Promise { return this.discover({ enabled: true }) } /** * Search augmentations by keyword * @param query Search query * @returns Matching augmentations */ async search(query: string): Promise { const all = await this.discover() const queryLower = query.toLowerCase() return all.filter(listing => { const manifest = listing.manifest // Search in various fields const searchFields = [ manifest.name, manifest.description, manifest.longDescription, ...(manifest.keywords || []), manifest.category ].filter(Boolean).map(s => s!.toLowerCase()) return searchFields.some(field => field.includes(queryLower)) }) } /** * Export configuration for all augmentations * @returns Map of augmentation IDs to configurations */ async exportConfigurations(): Promise> { const configs: Record = {} const listings = await this.discover({ includeConfig: true }) for (const listing of listings) { if (listing.config?.current) { configs[listing.id] = listing.config.current } } return configs } /** * Import configurations for multiple augmentations * @param configs Map of augmentation IDs to configurations * @returns Results of import operation */ async importConfigurations(configs: Record): Promise> { const results: Record = {} for (const [augId, config] of Object.entries(configs)) { try { // Validate before applying const validation = await this.validateConfig(augId, config) if (!validation.valid) { results[augId] = { success: false, error: `Validation failed: ${validation.errors?.join(', ')}` } continue } // Apply configuration await this.updateConfig(augId, config) results[augId] = { success: true } } catch (error) { results[augId] = { success: false, error: error instanceof Error ? error.message : String(error) } } } return results } /** * Generate configuration documentation * @param augId Augmentation ID * @returns Markdown documentation */ async generateConfigDocs(augId: string): Promise { const manifest = await this.getManifest(augId) if (!manifest) return null const schema = manifest.configSchema const examples = manifest.configExamples || [] const envVars = await this.getEnvironmentVariables(augId) let docs = `# ${manifest.name} Configuration\n\n` docs += `${manifest.description}\n\n` if (manifest.longDescription) { docs += `## Overview\n\n${manifest.longDescription}\n\n` } // Configuration options if (schema?.properties) { docs += `## Configuration Options\n\n` for (const [key, prop] of Object.entries(schema.properties)) { const required = schema.required?.includes(key) ? ' *(required)*' : '' docs += `### \`${key}\`${required}\n\n` if (prop.description) { docs += `${prop.description}\n\n` } docs += `- **Type**: ${prop.type}\n` if (prop.default !== undefined) { docs += `- **Default**: \`${JSON.stringify(prop.default)}\`\n` } if (prop.minimum !== undefined) { docs += `- **Minimum**: ${prop.minimum}\n` } if (prop.maximum !== undefined) { docs += `- **Maximum**: ${prop.maximum}\n` } if (prop.enum) { docs += `- **Allowed values**: ${prop.enum.map(v => `\`${v}\``).join(', ')}\n` } docs += '\n' } } // Environment variables if (envVars && Object.keys(envVars).length > 0) { docs += `## Environment Variables\n\n` docs += `| Variable | Config Key | Type | Required | Default |\n` docs += `|----------|------------|------|----------|----------|\n` for (const [envKey, info] of Object.entries(envVars)) { docs += `| \`${envKey}\` | ${info.configKey} | ${info.type} | ${info.required ? 'Yes' : 'No'} | ${info.default !== undefined ? `\`${info.default}\`` : '-'} |\n` } docs += '\n' } // Examples if (examples.length > 0) { docs += `## Examples\n\n` for (const example of examples) { docs += `### ${example.name}\n\n` if (example.description) { docs += `${example.description}\n\n` } docs += '```json\n' docs += JSON.stringify(example.config, null, 2) docs += '\n```\n\n' } } return docs } }