441 lines
16 KiB
JavaScript
441 lines
16 KiB
JavaScript
|
|
/**
|
||
|
|
* Augmentation Discovery API
|
||
|
|
*
|
||
|
|
* Provides discovery and configuration capabilities for augmentations
|
||
|
|
* Enables tools like brain-cloud to dynamically discover, configure, and manage augmentations
|
||
|
|
*/
|
||
|
|
/**
|
||
|
|
* Augmentation Discovery API
|
||
|
|
*
|
||
|
|
* Provides a unified interface for discovering and managing augmentations
|
||
|
|
*/
|
||
|
|
export class AugmentationDiscovery {
|
||
|
|
constructor(registry) {
|
||
|
|
this.registry = registry;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Discover all registered augmentations with manifests
|
||
|
|
* @param options Discovery options
|
||
|
|
* @returns List of augmentation listings
|
||
|
|
*/
|
||
|
|
async discover(options = {}) {
|
||
|
|
const augmentations = this.registry.getAll();
|
||
|
|
const listings = [];
|
||
|
|
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;
|
||
|
|
const manifest = getManifestFn();
|
||
|
|
// Apply filters
|
||
|
|
if (options.category && manifest.category !== options.category) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (options.enabled !== undefined) {
|
||
|
|
const isEnabled = aug.enabled !== false;
|
||
|
|
if (isEnabled !== options.enabled) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Build listing
|
||
|
|
const listing = {
|
||
|
|
id: manifest.id,
|
||
|
|
name: manifest.name,
|
||
|
|
manifest,
|
||
|
|
status: {
|
||
|
|
enabled: aug.enabled !== false,
|
||
|
|
initialized: aug.isInitialized || false,
|
||
|
|
category: aug.category || manifest.category,
|
||
|
|
priority: aug.priority
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// Include configuration if requested
|
||
|
|
if (options.includeConfig && 'getConfig' in aug) {
|
||
|
|
const getConfigFn = aug.getConfig;
|
||
|
|
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) {
|
||
|
|
const aug = this.registry.get(augId);
|
||
|
|
if (!aug || !('getManifest' in aug)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const getManifestFn = aug.getManifest;
|
||
|
|
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) {
|
||
|
|
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) {
|
||
|
|
const aug = this.registry.get(augId);
|
||
|
|
if (!aug || !('getConfig' in aug)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const getConfigFn = aug.getConfig;
|
||
|
|
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, config) {
|
||
|
|
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;
|
||
|
|
await updateConfigFn(config);
|
||
|
|
const getConfigFn = aug.getConfig;
|
||
|
|
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, config) {
|
||
|
|
const schema = await this.getConfigSchema(augId);
|
||
|
|
if (!schema) {
|
||
|
|
return {
|
||
|
|
valid: true,
|
||
|
|
warnings: ['No schema available for validation']
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const errors = [];
|
||
|
|
const warnings = [];
|
||
|
|
const suggestions = [];
|
||
|
|
// 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
|
||
|
|
*/
|
||
|
|
validatePropertyValue(key, value, schema, errors, warnings) {
|
||
|
|
// 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) {
|
||
|
|
const manifest = await this.getManifest(augId);
|
||
|
|
if (!manifest?.configSchema?.properties) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const prefix = `BRAINY_AUG_${augId.toUpperCase()}_`;
|
||
|
|
const vars = {};
|
||
|
|
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) {
|
||
|
|
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) {
|
||
|
|
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) {
|
||
|
|
return this.discover({ category });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get enabled augmentations
|
||
|
|
* @returns List of enabled augmentations
|
||
|
|
*/
|
||
|
|
async getEnabled() {
|
||
|
|
return this.discover({ enabled: true });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search augmentations by keyword
|
||
|
|
* @param query Search query
|
||
|
|
* @returns Matching augmentations
|
||
|
|
*/
|
||
|
|
async search(query) {
|
||
|
|
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() {
|
||
|
|
const configs = {};
|
||
|
|
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) {
|
||
|
|
const results = {};
|
||
|
|
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) {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=discovery.js.map
|