UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

605 lines (504 loc) 17 kB
/** * Plugin Registry * * Manages plugin lifecycle, dependency resolution, and extension point collection. */ import { EventEmitter } from 'events'; import type { PluginContext, PluginConfig, PluginMetadata, IEventBus, ILogger, ServiceContainer, AgentTypeDefinition, TaskTypeDefinition, MCPToolDefinition, CLICommandDefinition, MemoryBackendFactory, HookDefinition, WorkerDefinition, LLMProviderDefinition, HealthCheckResult, } from '../types/index.js'; import type { IPlugin, PluginFactory } from '../core/plugin-interface.js'; import { validatePlugin, PLUGIN_EVENTS } from '../core/plugin-interface.js'; // ============================================================================ // Registry Types // ============================================================================ export interface PluginRegistryConfig { coreVersion: string; dataDir: string; logger?: ILogger; eventBus?: IEventBus; defaultConfig?: Partial<PluginConfig>; maxPlugins?: number; loadTimeout?: number; } export interface PluginEntry { plugin: IPlugin; config: PluginConfig; loadTime: Date; initTime?: Date; error?: string; } export interface RegistryStats { total: number; initialized: number; failed: number; agentTypes: number; taskTypes: number; mcpTools: number; cliCommands: number; hooks: number; workers: number; providers: number; } // ============================================================================ // Default Implementations // ============================================================================ class DefaultEventBus implements IEventBus { private emitter = new EventEmitter(); emit(event: string, data?: unknown): void { this.emitter.emit(event, data); } on(event: string, handler: (data?: unknown) => void | Promise<void>): () => void { this.emitter.on(event, handler); return () => this.off(event, handler); } off(event: string, handler: (data?: unknown) => void | Promise<void>): void { this.emitter.off(event, handler); } once(event: string, handler: (data?: unknown) => void | Promise<void>): () => void { this.emitter.once(event, handler); return () => this.off(event, handler); } } class DefaultLogger implements ILogger { private context: Record<string, unknown> = {}; constructor(context?: Record<string, unknown>) { if (context) this.context = context; } debug(message: string, ...args: unknown[]): void { console.debug(`[DEBUG]`, message, ...args, this.context); } info(message: string, ...args: unknown[]): void { console.info(`[INFO]`, message, ...args, this.context); } warn(message: string, ...args: unknown[]): void { console.warn(`[WARN]`, message, ...args, this.context); } error(message: string, ...args: unknown[]): void { console.error(`[ERROR]`, message, ...args, this.context); } child(context: Record<string, unknown>): ILogger { return new DefaultLogger({ ...this.context, ...context }); } } class DefaultServiceContainer implements ServiceContainer { private services = new Map<string, unknown>(); get<T>(key: string): T | undefined { return this.services.get(key) as T | undefined; } set<T>(key: string, value: T): void { this.services.set(key, value); } has(key: string): boolean { return this.services.has(key); } delete(key: string): boolean { return this.services.delete(key); } } // ============================================================================ // Plugin Registry // ============================================================================ /** * Central registry for plugin management. * * Features: * - Plugin loading and unloading * - Dependency resolution * - Extension point collection * - Health monitoring * - Lifecycle management */ export class PluginRegistry extends EventEmitter { // ========================================================================= // Properties // ========================================================================= private readonly plugins = new Map<string, PluginEntry>(); private readonly config: PluginRegistryConfig; private readonly logger: ILogger; private readonly eventBus: IEventBus; private readonly services: ServiceContainer; private initialized = false; // Extension point caches private agentTypesCache: AgentTypeDefinition[] = []; private taskTypesCache: TaskTypeDefinition[] = []; private mcpToolsCache: MCPToolDefinition[] = []; private cliCommandsCache: CLICommandDefinition[] = []; private memoryBackendsCache: MemoryBackendFactory[] = []; private hooksCache: HookDefinition[] = []; private workersCache: WorkerDefinition[] = []; private providersCache: LLMProviderDefinition[] = []; // ========================================================================= // Constructor // ========================================================================= constructor(config: PluginRegistryConfig) { super(); this.config = config; this.logger = config.logger ?? new DefaultLogger({ component: 'PluginRegistry' }); this.eventBus = config.eventBus ?? new DefaultEventBus(); this.services = new DefaultServiceContainer(); // Register self in services this.services.set('pluginRegistry', this); } // ========================================================================= // Plugin Loading // ========================================================================= /** * Register a plugin. */ async register( plugin: IPlugin | PluginFactory, config?: Partial<PluginConfig> ): Promise<void> { // Resolve factory if needed const resolvedPlugin = typeof plugin === 'function' ? await plugin() : plugin; // Validate plugin if (!validatePlugin(resolvedPlugin)) { throw new Error('Invalid plugin: does not implement IPlugin interface'); } const name = resolvedPlugin.metadata.name; // Check for duplicates if (this.plugins.has(name)) { throw new Error(`Plugin ${name} already registered`); } // Check max plugins if (this.config.maxPlugins && this.plugins.size >= this.config.maxPlugins) { throw new Error(`Maximum plugin limit (${this.config.maxPlugins}) reached`); } // Create config const pluginConfig: PluginConfig = { enabled: true, priority: 50, settings: {}, ...this.config.defaultConfig, ...config, }; // Store entry const entry: PluginEntry = { plugin: resolvedPlugin, config: pluginConfig, loadTime: new Date(), }; this.plugins.set(name, entry); this.eventBus.emit(PLUGIN_EVENTS.LOADED, { plugin: name }); this.logger.info(`Plugin registered: ${name} v${resolvedPlugin.metadata.version}`); } /** * Unregister a plugin. */ async unregister(name: string): Promise<void> { const entry = this.plugins.get(name); if (!entry) { throw new Error(`Plugin ${name} not found`); } // Shutdown if initialized if (entry.plugin.state === 'initialized') { await entry.plugin.shutdown(); } this.plugins.delete(name); this.invalidateCaches(); this.logger.info(`Plugin unregistered: ${name}`); } // ========================================================================= // Initialization // ========================================================================= /** * Initialize all registered plugins. */ async initialize(): Promise<void> { if (this.initialized) { throw new Error('Registry already initialized'); } // Resolve dependencies and get load order const loadOrder = this.resolveDependencies(); // Initialize plugins in order for (const name of loadOrder) { const entry = this.plugins.get(name)!; if (!entry.config.enabled) { this.logger.info(`Plugin ${name} is disabled, skipping initialization`); continue; } try { const context = this.createPluginContext(entry); await this.initializeWithTimeout(entry.plugin, context); entry.initTime = new Date(); this.collectExtensionPoints(entry.plugin); this.logger.info(`Plugin initialized: ${name}`); } catch (error) { entry.error = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to initialize plugin ${name}: ${entry.error}`); // Continue with other plugins } } this.initialized = true; this.logger.info(`Registry initialized with ${this.plugins.size} plugins`); } /** * Shutdown all plugins. */ async shutdown(): Promise<void> { // Shutdown in reverse order const names = Array.from(this.plugins.keys()).reverse(); for (const name of names) { const entry = this.plugins.get(name)!; if (entry.plugin.state === 'initialized') { try { await entry.plugin.shutdown(); this.logger.info(`Plugin shutdown: ${name}`); } catch (error) { this.logger.error(`Error shutting down plugin ${name}: ${error}`); } } } this.invalidateCaches(); this.initialized = false; } // ========================================================================= // Dependency Resolution // ========================================================================= /** * Resolve dependencies and return load order. */ private resolveDependencies(): string[] { const visited = new Set<string>(); const visiting = new Set<string>(); const order: string[] = []; const visit = (name: string): void => { if (visited.has(name)) return; if (visiting.has(name)) { throw new Error(`Circular dependency detected: ${name}`); } const entry = this.plugins.get(name); if (!entry) { throw new Error(`Missing dependency: ${name}`); } visiting.add(name); const deps = entry.plugin.metadata.dependencies ?? []; for (const dep of deps) { visit(dep); } visiting.delete(name); visited.add(name); order.push(name); }; for (const name of this.plugins.keys()) { visit(name); } return order; } // ========================================================================= // Extension Points // ========================================================================= /** * Collect extension points from a plugin. */ private collectExtensionPoints(plugin: IPlugin): void { if (plugin.registerAgentTypes) { const types = plugin.registerAgentTypes(); if (types) this.agentTypesCache.push(...types); } if (plugin.registerTaskTypes) { const types = plugin.registerTaskTypes(); if (types) this.taskTypesCache.push(...types); } if (plugin.registerMCPTools) { const tools = plugin.registerMCPTools(); if (tools) this.mcpToolsCache.push(...tools); } if (plugin.registerCLICommands) { const commands = plugin.registerCLICommands(); if (commands) this.cliCommandsCache.push(...commands); } if (plugin.registerMemoryBackends) { const backends = plugin.registerMemoryBackends(); if (backends) this.memoryBackendsCache.push(...backends); } if (plugin.registerHooks) { const hooks = plugin.registerHooks(); if (hooks) this.hooksCache.push(...hooks); } if (plugin.registerWorkers) { const workers = plugin.registerWorkers(); if (workers) this.workersCache.push(...workers); } if (plugin.registerProviders) { const providers = plugin.registerProviders(); if (providers) this.providersCache.push(...providers); } } /** * Invalidate extension point caches. */ private invalidateCaches(): void { this.agentTypesCache = []; this.taskTypesCache = []; this.mcpToolsCache = []; this.cliCommandsCache = []; this.memoryBackendsCache = []; this.hooksCache = []; this.workersCache = []; this.providersCache = []; // Recollect from initialized plugins for (const entry of this.plugins.values()) { if (entry.plugin.state === 'initialized') { this.collectExtensionPoints(entry.plugin); } } } // ========================================================================= // Getters // ========================================================================= getAgentTypes(): AgentTypeDefinition[] { return [...this.agentTypesCache]; } getTaskTypes(): TaskTypeDefinition[] { return [...this.taskTypesCache]; } getMCPTools(): MCPToolDefinition[] { return [...this.mcpToolsCache]; } getCLICommands(): CLICommandDefinition[] { return [...this.cliCommandsCache]; } getMemoryBackends(): MemoryBackendFactory[] { return [...this.memoryBackendsCache]; } getHooks(): HookDefinition[] { return [...this.hooksCache]; } getWorkers(): WorkerDefinition[] { return [...this.workersCache]; } getProviders(): LLMProviderDefinition[] { return [...this.providersCache]; } getPlugin(name: string): IPlugin | undefined { return this.plugins.get(name)?.plugin; } getPluginEntry(name: string): PluginEntry | undefined { return this.plugins.get(name); } listPlugins(): PluginMetadata[] { return Array.from(this.plugins.values()).map(e => e.plugin.metadata); } // ========================================================================= // Health Check // ========================================================================= /** * Run health checks on all plugins. */ async healthCheck(): Promise<Map<string, HealthCheckResult>> { const results = new Map<string, HealthCheckResult>(); for (const [name, entry] of this.plugins) { if (entry.plugin.state !== 'initialized') { results.set(name, { healthy: false, status: 'unhealthy', message: `Plugin not initialized: ${entry.plugin.state}`, checks: {}, timestamp: new Date(), }); continue; } try { if (entry.plugin.healthCheck) { results.set(name, await entry.plugin.healthCheck()); } else { results.set(name, { healthy: true, status: 'healthy', checks: {}, timestamp: new Date(), }); } } catch (error) { results.set(name, { healthy: false, status: 'unhealthy', message: error instanceof Error ? error.message : String(error), checks: {}, timestamp: new Date(), }); } } return results; } // ========================================================================= // Stats // ========================================================================= getStats(): RegistryStats { let initialized = 0; let failed = 0; for (const entry of this.plugins.values()) { if (entry.plugin.state === 'initialized') initialized++; if (entry.plugin.state === 'error' || entry.error) failed++; } return { total: this.plugins.size, initialized, failed, agentTypes: this.agentTypesCache.length, taskTypes: this.taskTypesCache.length, mcpTools: this.mcpToolsCache.length, cliCommands: this.cliCommandsCache.length, hooks: this.hooksCache.length, workers: this.workersCache.length, providers: this.providersCache.length, }; } // ========================================================================= // Helpers // ========================================================================= private createPluginContext(entry: PluginEntry): PluginContext { return { config: entry.config, eventBus: this.eventBus, logger: this.logger.child({ plugin: entry.plugin.metadata.name }), services: this.services, coreVersion: this.config.coreVersion, dataDir: this.config.dataDir, }; } private async initializeWithTimeout( plugin: IPlugin, context: PluginContext ): Promise<void> { const timeout = this.config.loadTimeout ?? 30000; await Promise.race([ plugin.initialize(context), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Initialization timeout')), timeout) ), ]); } } // ============================================================================ // Default Registry Instance // ============================================================================ let defaultRegistry: PluginRegistry | null = null; export function getDefaultRegistry(): PluginRegistry { if (!defaultRegistry) { defaultRegistry = new PluginRegistry({ coreVersion: '3.0.0', dataDir: process.cwd(), }); } return defaultRegistry; } export function setDefaultRegistry(registry: PluginRegistry): void { defaultRegistry = registry; }