UNPKG

@mrtkrcm/acp-claude-code

Version:

ACP (Agent Client Protocol) bridge for Claude Code

463 lines 17.5 kB
import * as fs from "fs"; import * as path from "path"; import { EventEmitter } from "events"; import * as os from "node:os"; import { createLogger } from "../logger.js"; /** * Manages plugin lifecycle, loading, and execution */ export class PluginManager { plugins = new Map(); pluginDirectories = []; pluginMiddleware = []; eventEmitter; logger; constructor() { this.logger = createLogger("PluginManager"); this.eventEmitter = new EventEmitter(); this.setupEventHandlers(); } /** * Initializes the plugin system */ async initializePluginSystem() { // Set up default plugin directories this.pluginDirectories = [ path.join(process.cwd(), "plugins"), path.join(process.cwd(), ".acp-plugins"), path.join(os.homedir(), ".acp-plugins"), ]; // Ensure plugin directories exist this.pluginDirectories.forEach((dir) => { try { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } catch (error) { this.logger.warn(`Failed to create plugin directory ${dir}: ${error}`); } }); // Load plugins from all directories for (const dir of this.pluginDirectories) { await this.discoverAndLoadPlugins(dir); } this.logger.info(`Plugin system initialized with ${this.plugins.size} plugins loaded`); } /** * Discovers and loads plugins from a directory */ async discoverAndLoadPlugins(directory) { try { if (!fs.existsSync(directory)) { return; } const entries = fs.readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const pluginPath = path.join(directory, entry.name); this.loadPlugin(pluginPath); } else if (entry.name.endsWith(".js") || entry.name.endsWith(".ts")) { // Direct plugin file const pluginPath = path.join(directory, entry.name); await this.loadPluginFile(pluginPath); } } } catch (error) { this.logger.warn(`Failed to discover plugins in ${directory}: ${error}`); } } /** * Loads a plugin from a directory */ loadPlugin(pluginPath) { try { const manifestPath = path.join(pluginPath, "manifest.json"); if (!fs.existsSync(manifestPath)) { return; // Not a valid plugin directory } const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); const mainPath = path.join(pluginPath, manifest.main); if (!fs.existsSync(mainPath)) { this.logger.warn(`Plugin ${manifest.name}: main file ${manifest.main} not found`); return; } this.loadPluginFile(mainPath, manifest); } catch (error) { this.logger.warn(`Failed to load plugin from ${pluginPath}: ${error}`); } } /** * Loads a plugin from a file */ async loadPluginFile(filePath, manifest) { try { // Use dynamic import for ES6 modules const pluginModule = await import(filePath); if (!pluginModule || typeof pluginModule !== "object") { this.logger.warn(`Plugin file ${filePath} does not export a valid plugin`); return; } const pluginName = manifest?.name || path.basename(filePath, path.extname(filePath)); const pluginInstance = { manifest: manifest || { name: pluginName, version: pluginModule.version || "1.0.0", description: pluginModule.description || "Custom plugin", author: pluginModule.author || "Unknown", main: path.basename(filePath), capabilities: pluginModule.capabilities || [], }, tools: pluginModule.tools || [], middleware: pluginModule.middleware || [], state: "loaded", loadedAt: Date.now(), metadata: pluginModule.metadata || {}, }; this.plugins.set(pluginName, pluginInstance); this.logger.info(`Loaded plugin: ${pluginName} v${pluginInstance.manifest.version}`); // Register middleware if (pluginInstance.middleware) { pluginInstance.middleware.forEach((middleware) => { this.pluginMiddleware.push(middleware); this.pluginMiddleware.sort((a, b) => a.priority - b.priority); }); } // Emit plugin loaded event this.eventEmitter.emit("pluginLoaded", pluginInstance); } catch (error) { this.logger.warn(`Failed to load plugin file ${filePath}: ${error}`); } } /** * Executes a plugin tool */ async executePluginTool(pluginName, toolName, input, context) { const plugin = this.plugins.get(pluginName); if (!plugin || plugin.state !== "active") { return { success: false, output: null, error: `Plugin ${pluginName} not found or not active`, }; } const tool = plugin.tools.find((t) => t.name === toolName); if (!tool) { return { success: false, output: null, error: `Tool ${toolName} not found in plugin ${pluginName}`, }; } try { // Validate input if validator exists if (tool.validate) { const validationResult = await tool.validate(input); if (!validationResult.valid) { return { success: false, output: null, error: `Validation failed: ${validationResult.errors?.join(", ")}`, }; } } // Execute through middleware chain const result = await this.executeThroughMiddleware(tool, input, context); // Update plugin metrics plugin.metadata.lastExecution = Date.now(); plugin.metadata.executionCount = (plugin.metadata.executionCount || 0) + 1; return result; } catch (error) { this.logger.error(`Plugin tool execution failed: ${pluginName}.${toolName}`, error); return { success: false, output: null, error: `Execution failed: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Executes a tool through the middleware chain */ async executeThroughMiddleware(tool, input, context) { let index = 0; const next = async () => { if (index < this.pluginMiddleware.length) { const middleware = this.pluginMiddleware[index++]; return middleware.intercept(context, next); } else { // Execute the actual tool const startTime = Date.now(); const result = await tool.execute(input, context); result.duration = Date.now() - startTime; return result; } }; return next(); } /** * Lists all available plugins */ getAvailablePlugins() { return Array.from(this.plugins.values()) .filter((plugin) => plugin.state === "active") .map((plugin) => ({ name: plugin.manifest.name, description: plugin.manifest.description, version: plugin.manifest.version, tools: plugin.tools.map((tool) => tool.name), })); } /** * Enables a plugin */ enablePlugin(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { return false; } plugin.state = "active"; this.eventEmitter.emit("pluginEnabled", plugin); this.logger.info(`Enabled plugin: ${pluginName}`); return true; } /** * Disables a plugin */ disablePlugin(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { return false; } plugin.state = "disabled"; this.eventEmitter.emit("pluginDisabled", plugin); this.logger.info(`Disabled plugin: ${pluginName}`); return true; } /** * Gets plugin information */ getPluginInfo(pluginName) { return this.plugins.get(pluginName) || null; } /** * Handles plugin tool output */ async handlePluginToolOutput(sessionId, msg, pluginName, toolName, streamingOperationId, completeStreamingOperation, sendSessionUpdate, sendAgentThought) { const toolCallId = typeof msg.id === "string" ? msg.id : String(msg.id || ""); try { // Execute plugin tool with enhanced permission system const pluginContext = { sessionId, toolCallId, permissions: this.getPluginPermissions(pluginName, toolName, sessionId), metadata: { timestamp: Date.now(), operationType: this.getPluginOperationType(pluginName, toolName), complexity: this.analyzePluginComplexity(pluginName, toolName, msg.input), }, }; const result = await this.executePluginTool(pluginName, toolName, msg.input, pluginContext); // Format plugin tool output const enhancedContent = this.formatPluginToolOutput(result, pluginName, toolName); // Send agent thought for plugin completion if (sendAgentThought) { const thought = result.success ? `Successfully executed ${pluginName}.${toolName} plugin` : `Plugin execution failed: ${pluginName}.${toolName}`; await sendAgentThought(sessionId, thought); } // Complete streaming operation if (streamingOperationId && completeStreamingOperation) { completeStreamingOperation(streamingOperationId, result.success); } if (sendSessionUpdate) { await sendSessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: result.success ? "completed" : "failed", content: [ { type: "content", content: { type: "text", text: enhancedContent, }, }, ], }, }); } } catch (error) { this.logger.error(`Plugin tool execution failed: ${pluginName}.${toolName}`, error); if (sendSessionUpdate) { await sendSessionUpdate({ sessionId, update: { sessionUpdate: "tool_call_update", toolCallId, status: "failed", content: [ { type: "content", content: { type: "text", text: `[PLUGIN-ERROR] ${pluginName}.${toolName}: ${error instanceof Error ? error.message : String(error)}`, }, }, ], }, }); } } } /** * Formats plugin tool output for display */ formatPluginToolOutput(result, pluginName, toolName) { const status = result.success ? "[PLUGIN-SUCCESS]" : "[PLUGIN-ERROR]"; const duration = result.duration ? ` (${result.duration}ms)` : ""; let output = `${status} ${pluginName}.${toolName}${duration}\n`; if (result.success) { if (typeof result.output === "string") { output += result.output; } else if (result.output) { output += JSON.stringify(result.output, null, 2); } } else { output += `Error: ${result.error || "Unknown error"}`; } // Add metadata if available if (result.metadata) { const metadataStr = Object.entries(result.metadata) .map(([key, value]) => `${key}: ${value}`) .join(", "); if (metadataStr) { output += `\n[METADATA] ${metadataStr}`; } } return output; } /** * Sets up event handlers for plugin lifecycle */ setupEventHandlers() { this.eventEmitter.on("pluginLoaded", (plugin) => { this.logger.info(`Plugin event: ${plugin.manifest.name} loaded`); }); this.eventEmitter.on("pluginEnabled", (plugin) => { this.logger.info(`Plugin event: ${plugin.manifest.name} enabled`); }); this.eventEmitter.on("pluginDisabled", (plugin) => { this.logger.info(`Plugin event: ${plugin.manifest.name} disabled`); }); } /** * Gets the event emitter for external event handling */ getEventEmitter() { return this.eventEmitter; } /** * Gets permissions for a specific plugin tool */ getPluginPermissions(pluginName, toolName, sessionId) { const plugin = this.plugins.get(pluginName); if (!plugin) return []; // Get tool-specific permissions const tool = plugin.tools.find((t) => t.name === toolName); if (!tool) return []; // Default permissions based on tool capabilities const permissions = []; if (tool.capabilities?.some((cap) => cap.operations.includes("read"))) { permissions.push("read_files"); } if (tool.capabilities?.some((cap) => cap.operations.includes("write"))) { permissions.push("write_files"); } if (tool.capabilities?.some((cap) => cap.operations.includes("execute"))) { permissions.push("execute_commands"); } if (tool.capabilities?.some((cap) => cap.operations.includes("network"))) { permissions.push("network_access"); } // Add session-specific permissions if (sessionId) { permissions.push(`session_${sessionId}`); } return permissions; } /** * Gets operation type for a plugin tool */ getPluginOperationType(pluginName, toolName) { const plugin = this.plugins.get(pluginName); if (!plugin) return "unknown"; const tool = plugin.tools.find((t) => t.name === toolName); if (!tool) return "unknown"; // Determine operation type based on tool capabilities if (tool.capabilities?.some((cap) => cap.operations.includes("read"))) return "read"; if (tool.capabilities?.some((cap) => cap.operations.includes("write"))) return "write"; if (tool.capabilities?.some((cap) => cap.operations.includes("execute"))) return "execute"; if (tool.capabilities?.some((cap) => cap.operations.includes("network"))) return "network"; return "other"; } /** * Analyzes complexity of plugin operation */ analyzePluginComplexity(pluginName, toolName, input) { const plugin = this.plugins.get(pluginName); if (!plugin) return "simple"; const tool = plugin.tools.find((t) => t.name === toolName); if (!tool) return "simple"; // Analyze input complexity if (!input || typeof input !== "object") return "simple"; const inputObj = input; const keys = Object.keys(inputObj); // Complex if many parameters or large data if (keys.length > 5) return "complex"; if (keys.length > 2) return "moderate"; // Check for complex data types for (const value of Object.values(inputObj)) { if (Array.isArray(value) && value.length > 10) return "complex"; if (typeof value === "string" && value.length > 1000) return "complex"; } return "simple"; } /** * Cleans up plugin resources */ cleanup() { this.plugins.clear(); this.pluginMiddleware.length = 0; this.eventEmitter.removeAllListeners(); this.logger.info("Plugin manager cleaned up"); } } //# sourceMappingURL=PluginManager.js.map