aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
627 lines (548 loc) • 18.8 kB
JavaScript
/**
* Component Metadata Manager for Synkra AIOS
* Manages detailed metadata for all framework components
* @module component-metadata
*/
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
// Optional memory adapter - gracefully handle if not available
let MemoryAdapter = null;
try {
({ MemoryAdapter } = require('../../memory'));
} catch (e) {
// Memory module not available - will use null adapter
}
class ComponentMetadata {
constructor(options = {}) {
this.rootPath = options.rootPath || process.cwd();
this.metadataPath = path.join(this.rootPath, 'aios-core', 'metadata');
// Initialize memory adapter if available
this.memoryClient = MemoryAdapter ? new MemoryAdapter({
persistencePath: path.join(this.rootPath, 'aios-memory-layer-mvp', 'data'),
namespace: 'component-metadata',
}) : null;
// Component metadata schema version
this.schemaVersion = '1.0';
}
/**
* Get metadata schema for a component type
* @param {string} componentType - Type of component (agent/task/workflow)
* @returns {Object} Metadata schema
*/
getMetadataSchema(componentType) {
const baseSchema = {
id: null,
type: componentType,
name: null,
version: '1.0',
created: {
timestamp: null,
creator: null,
context: null,
},
modified: {
timestamp: null,
modifier: null,
changes: [],
},
description: null,
tags: [],
status: 'active', // active, deprecated, experimental
visibility: 'private', // private, team, public
relationships: {
dependencies: [],
dependents: [],
related: [],
},
usage: {
count: 0,
lastUsed: null,
frequency: 'never', // never, rarely, occasionally, frequently, always
contexts: [],
},
metrics: {
complexity: 0, // 0-10 scale
maintainability: 0, // 0-10 scale
reliability: 0, // 0-10 scale
performance: null,
},
security: {
level: 'standard', // standard, elevated, restricted
permissions: [],
auditLog: [],
},
customFields: {},
};
// Add type-specific fields
switch (componentType) {
case 'agent':
return {
...baseSchema,
agentSpecific: {
commands: [],
persona: {},
capabilities: [],
integrations: [],
},
};
case 'task':
return {
...baseSchema,
taskSpecific: {
agent: null,
command: null,
workflow: null,
inputSchema: {},
outputSchema: {},
errorCodes: [],
},
};
case 'workflow':
return {
...baseSchema,
workflowSpecific: {
steps: [],
triggers: [],
variables: {},
outcomes: [],
branchingLogic: {},
},
};
default:
return baseSchema;
}
}
/**
* Create metadata for a new component
* @param {string} componentType - Type of component
* @param {Object} componentData - Component configuration data
* @param {Object} context - Creation context
* @returns {Promise<Object>} Created metadata
*/
async createMetadata(componentType, componentData, context = {}) {
try {
const schema = this.getMetadataSchema(componentType);
// Populate metadata
const metadata = {
...schema,
id: componentData.id || componentData.name || componentData.agentName || componentData.taskId || componentData.workflowId,
name: componentData.name || componentData.title || componentData.agentTitle || componentData.taskTitle || componentData.workflowName,
created: {
timestamp: new Date().toISOString(),
creator: context.creator || process.env.USER || 'system',
context: context.description || 'Component created',
},
modified: {
timestamp: new Date().toISOString(),
modifier: context.creator || process.env.USER || 'system',
changes: [{
timestamp: new Date().toISOString(),
type: 'created',
description: 'Initial creation',
}],
},
description: componentData.description || componentData.whenToUse || componentData.taskDescription || componentData.workflowDescription || '',
tags: context.tags || [],
status: context.status || 'active',
visibility: context.visibility || 'private',
};
// Add type-specific data
this.addTypeSpecificData(componentType, metadata, componentData);
// Save to memory layer
await this.memoryClient.addMemory({
type: 'component_metadata',
component: componentType,
metadata: metadata,
});
// Save to file system
await this.saveMetadataToFile(componentType, metadata.id, metadata);
return metadata;
} catch (error) {
console.error(chalk.red(`Failed to create metadata: ${error.message}`));
throw error;
}
}
/**
* Add type-specific data to metadata
* @private
*/
addTypeSpecificData(componentType, metadata, componentData) {
switch (componentType) {
case 'agent':
if (metadata.agentSpecific) {
metadata.agentSpecific.commands = componentData.commands || [];
metadata.agentSpecific.persona = componentData.persona || {};
metadata.agentSpecific.capabilities = this.extractCapabilities(componentData);
metadata.agentSpecific.integrations = componentData.dependencies || [];
}
break;
case 'task':
if (metadata.taskSpecific) {
metadata.taskSpecific.agent = componentData.agentName;
metadata.taskSpecific.command = componentData.command;
metadata.taskSpecific.workflow = componentData.workflow;
metadata.taskSpecific.inputSchema = componentData.inputSchema || {};
metadata.taskSpecific.outputSchema = componentData.outputSchema || {};
metadata.taskSpecific.errorCodes = componentData.errorCodes || [];
}
break;
case 'workflow':
if (metadata.workflowSpecific) {
metadata.workflowSpecific.steps = componentData.steps || [];
metadata.workflowSpecific.triggers = componentData.triggers || [];
metadata.workflowSpecific.variables = componentData.variables || {};
metadata.workflowSpecific.outcomes = componentData.outcomes || [];
metadata.workflowSpecific.branchingLogic = componentData.branchingLogic || {};
}
break;
}
}
/**
* Update metadata for an existing component
* @param {string} componentType - Type of component
* @param {string} componentId - Component ID
* @param {Object} updates - Updates to apply
* @param {Object} context - Update context
* @returns {Promise<Object>} Updated metadata
*/
async updateMetadata(componentType, componentId, updates, context = {}) {
try {
// Load existing metadata
const metadata = await this.getMetadata(componentType, componentId);
if (!metadata) {
throw new Error(`Metadata not found for ${componentType}: ${componentId}`);
}
// Apply updates
Object.assign(metadata, updates);
// Update modified fields
metadata.modified = {
timestamp: new Date().toISOString(),
modifier: context.modifier || process.env.USER || 'system',
changes: [
...metadata.modified.changes,
{
timestamp: new Date().toISOString(),
type: context.changeType || 'updated',
description: context.changeDescription || 'Component updated',
fields: Object.keys(updates),
},
],
};
// Update version if significant change
if (context.incrementVersion) {
metadata.version = this.incrementVersion(metadata.version);
}
// Save to memory layer
await this.memoryClient.updateMemory({
type: 'component_metadata',
component: componentType,
id: componentId,
metadata: metadata,
});
// Save to file system
await this.saveMetadataToFile(componentType, componentId, metadata);
return metadata;
} catch (error) {
console.error(chalk.red(`Failed to update metadata: ${error.message}`));
throw error;
}
}
/**
* Get metadata for a component
* @param {string} componentType - Type of component
* @param {string} componentId - Component ID
* @returns {Promise<Object|null>} Component metadata
*/
async getMetadata(componentType, componentId) {
try {
// Try memory layer first
const memories = await this.memoryClient.searchMemories({
filters: {
type: 'component_metadata',
component: componentType,
'metadata.id': componentId,
},
});
if (memories && memories.length > 0) {
return memories[0].metadata;
}
// Fallback to file system
const filePath = this.getMetadataFilePath(componentType, componentId);
if (await fs.pathExists(filePath)) {
return await fs.readJson(filePath);
}
return null;
} catch (error) {
console.error(chalk.red(`Failed to get metadata: ${error.message}`));
return null;
}
}
/**
* Track component relationship
* @param {Object} source - Source component {type, id}
* @param {Object} target - Target component {type, id}
* @param {string} relationshipType - Type of relationship (depends-on, used-by, related-to)
* @returns {Promise<void>}
*/
async trackRelationship(source, target, relationshipType) {
try {
// Update source component
const sourceMetadata = await this.getMetadata(source.type, source.id);
if (sourceMetadata) {
if (!sourceMetadata.relationships) {
sourceMetadata.relationships = { dependencies: [], dependents: [], related: [] };
}
const relationship = {
type: target.type,
id: target.id,
relationshipType,
timestamp: new Date().toISOString(),
};
switch (relationshipType) {
case 'depends-on':
sourceMetadata.relationships.dependencies.push(relationship);
break;
case 'used-by':
sourceMetadata.relationships.dependents.push(relationship);
break;
case 'related-to':
sourceMetadata.relationships.related.push(relationship);
break;
}
await this.updateMetadata(source.type, source.id, {
relationships: sourceMetadata.relationships,
});
}
// Update target component (reverse relationship)
const targetMetadata = await this.getMetadata(target.type, target.id);
if (targetMetadata) {
if (!targetMetadata.relationships) {
targetMetadata.relationships = { dependencies: [], dependents: [], related: [] };
}
const reverseRelationship = {
type: source.type,
id: source.id,
relationshipType: this.getReverseRelationship(relationshipType),
timestamp: new Date().toISOString(),
};
switch (reverseRelationship.relationshipType) {
case 'depends-on':
targetMetadata.relationships.dependencies.push(reverseRelationship);
break;
case 'used-by':
targetMetadata.relationships.dependents.push(reverseRelationship);
break;
case 'related-to':
targetMetadata.relationships.related.push(reverseRelationship);
break;
}
await this.updateMetadata(target.type, target.id, {
relationships: targetMetadata.relationships,
});
}
} catch (error) {
console.error(chalk.red(`Failed to track relationship: ${error.message}`));
throw error;
}
}
/**
* Search components by criteria
* @param {Object} criteria - Search criteria
* @returns {Promise<Array>} Matching components
*/
async searchComponents(criteria = {}) {
try {
const filters = {};
// Build filters
if (criteria.type) {
filters.component = criteria.type;
}
if (criteria.tags && criteria.tags.length > 0) {
filters['metadata.tags'] = { $in: criteria.tags };
}
if (criteria.status) {
filters['metadata.status'] = criteria.status;
}
if (criteria.creator) {
filters['metadata.created.creator'] = criteria.creator;
}
if (criteria.text) {
filters.$text = { $search: criteria.text };
}
// Search in memory layer
const memories = await this.memoryClient.searchMemories({
filters: {
type: 'component_metadata',
...filters,
},
limit: criteria.limit || 100,
});
return memories.map(m => m.metadata);
} catch (error) {
console.error(chalk.red(`Search failed: ${error.message}`));
return [];
}
}
/**
* Collect usage analytics for a component
* @param {string} componentType - Type of component
* @param {string} componentId - Component ID
* @param {Object} usageData - Usage information
* @returns {Promise<void>}
*/
async collectUsageAnalytics(componentType, componentId, usageData) {
try {
const metadata = await this.getMetadata(componentType, componentId);
if (!metadata) {
return;
}
// Update usage stats
if (!metadata.usage) {
metadata.usage = {
count: 0,
lastUsed: null,
frequency: 'never',
contexts: [],
};
}
metadata.usage.count++;
metadata.usage.lastUsed = new Date().toISOString();
// Add context if provided
if (usageData.context) {
metadata.usage.contexts.push({
timestamp: new Date().toISOString(),
context: usageData.context,
user: usageData.user || process.env.USER,
duration: usageData.duration,
success: usageData.success !== false,
});
// Keep only last 100 contexts
if (metadata.usage.contexts.length > 100) {
metadata.usage.contexts = metadata.usage.contexts.slice(-100);
}
}
// Update frequency based on usage patterns
metadata.usage.frequency = this.calculateFrequency(metadata.usage);
// Save updated metadata
await this.updateMetadata(componentType, componentId, {
usage: metadata.usage,
}, {
changeType: 'usage',
changeDescription: 'Usage analytics updated',
});
// Also log to memory layer for analytics
await this.memoryClient.addMemory({
type: 'component_usage',
component: componentType,
id: componentId,
timestamp: new Date().toISOString(),
...usageData,
});
} catch (error) {
console.error(chalk.red(`Failed to collect usage analytics: ${error.message}`));
}
}
/**
* Get component versioning history
* @param {string} componentType - Type of component
* @param {string} componentId - Component ID
* @returns {Promise<Array>} Version history
*/
async getVersionHistory(componentType, componentId) {
try {
const metadata = await this.getMetadata(componentType, componentId);
if (!metadata) {
return [];
}
// Extract version history from changes
const versionHistory = metadata.modified.changes
.filter(change => change.type === 'version')
.map(change => ({
version: change.version,
timestamp: change.timestamp,
modifier: change.modifier || metadata.modified.modifier,
description: change.description,
changes: change.fields || [],
}));
return versionHistory;
} catch (error) {
console.error(chalk.red(`Failed to get version history: ${error.message}`));
return [];
}
}
/**
* Extract capabilities from component data
* @private
*/
extractCapabilities(componentData) {
const capabilities = [];
if (componentData.commands) {
capabilities.push(...componentData.commands.map(cmd => `command:${cmd}`));
}
if (componentData.integrations) {
capabilities.push(...componentData.integrations.map(int => `integration:${int}`));
}
if (componentData.features) {
capabilities.push(...componentData.features);
}
return capabilities;
}
/**
* Get reverse relationship type
* @private
*/
getReverseRelationship(relationshipType) {
const reverseMap = {
'depends-on': 'used-by',
'used-by': 'depends-on',
'related-to': 'related-to',
};
return reverseMap[relationshipType] || relationshipType;
}
/**
* Calculate usage frequency
* @private
*/
calculateFrequency(usage) {
if (!usage.contexts || usage.contexts.length === 0) {
return 'never';
}
// Get usage in last 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentUsage = usage.contexts.filter(ctx =>
new Date(ctx.timestamp) > thirtyDaysAgo,
).length;
if (recentUsage === 0) return 'rarely';
if (recentUsage < 5) return 'occasionally';
if (recentUsage < 20) return 'frequently';
return 'always';
}
/**
* Increment version number
* @private
*/
incrementVersion(version) {
const parts = version.split('.');
const patch = parseInt(parts[2] || 0) + 1;
return `${parts[0]}.${parts[1]}.${patch}`;
}
/**
* Get metadata file path
* @private
*/
getMetadataFilePath(componentType, componentId) {
return path.join(this.metadataPath, componentType, `${componentId}.metadata.json`);
}
/**
* Save metadata to file
* @private
*/
async saveMetadataToFile(componentType, componentId, metadata) {
const filePath = this.getMetadataFilePath(componentType, componentId);
await fs.ensureDir(path.dirname(filePath));
await fs.writeJson(filePath, metadata, { spaces: 2 });
}
}
module.exports = ComponentMetadata;