279 lines
8.2 KiB
JavaScript
279 lines
8.2 KiB
JavaScript
/**
|
|
* Plugins System - Load and manage Claude Agent SDK plugins
|
|
*
|
|
* Supports loading plugins from:
|
|
* - Local filesystem
|
|
* - NPM packages
|
|
* - Remote URLs
|
|
* - In-memory definitions
|
|
*/
|
|
import { logger } from "../utils/logger.js";
|
|
import { existsSync, readFileSync } from "fs";
|
|
import { join, resolve } from "path";
|
|
// Plugin registry
|
|
const loadedPlugins = new Map();
|
|
/**
|
|
* Load a plugin from configuration
|
|
*/
|
|
export async function loadPlugin(config) {
|
|
try {
|
|
switch (config.type) {
|
|
case 'local':
|
|
return await loadLocalPlugin(config);
|
|
case 'npm':
|
|
return await loadNpmPlugin(config);
|
|
case 'remote':
|
|
return await loadRemotePlugin(config);
|
|
case 'inline':
|
|
return loadInlinePlugin(config);
|
|
default:
|
|
logger.error('Unknown plugin type', { config });
|
|
return null;
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.error('Failed to load plugin', { config, error: error.message });
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Load plugin from local filesystem
|
|
*/
|
|
async function loadLocalPlugin(config) {
|
|
const pluginPath = resolve(config.path);
|
|
if (!existsSync(pluginPath)) {
|
|
logger.error('Plugin path does not exist', { path: pluginPath });
|
|
return null;
|
|
}
|
|
// Look for package.json or plugin.json
|
|
const packageJsonPath = join(pluginPath, 'package.json');
|
|
const pluginJsonPath = join(pluginPath, 'plugin.json');
|
|
let metadata = { name: 'unknown', version: '0.0.0' };
|
|
if (existsSync(packageJsonPath)) {
|
|
metadata = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
}
|
|
else if (existsSync(pluginJsonPath)) {
|
|
metadata = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
|
|
}
|
|
// Try to load the plugin module
|
|
const mainPath = join(pluginPath, metadata.main || 'index.js');
|
|
if (!existsSync(mainPath)) {
|
|
logger.error('Plugin main file not found', { path: mainPath });
|
|
return null;
|
|
}
|
|
const module = await import(mainPath);
|
|
const tools = module.tools || module.default?.tools || [];
|
|
const plugin = {
|
|
name: metadata.name,
|
|
version: metadata.version,
|
|
source: `local:${pluginPath}`,
|
|
tools,
|
|
enabled: true,
|
|
loadedAt: Date.now()
|
|
};
|
|
loadedPlugins.set(plugin.name, plugin);
|
|
logger.info('Local plugin loaded', { name: plugin.name, tools: tools.length });
|
|
return plugin;
|
|
}
|
|
/**
|
|
* Load plugin from NPM package
|
|
*/
|
|
async function loadNpmPlugin(config) {
|
|
try {
|
|
const module = await import(config.package);
|
|
const metadata = module.default?.metadata || { name: config.package, version: config.version || '0.0.0' };
|
|
const tools = module.tools || module.default?.tools || [];
|
|
const plugin = {
|
|
name: metadata.name || config.package,
|
|
version: metadata.version || config.version || '0.0.0',
|
|
source: `npm:${config.package}`,
|
|
tools,
|
|
enabled: true,
|
|
loadedAt: Date.now()
|
|
};
|
|
loadedPlugins.set(plugin.name, plugin);
|
|
logger.info('NPM plugin loaded', { name: plugin.name, tools: tools.length });
|
|
return plugin;
|
|
}
|
|
catch (error) {
|
|
logger.error('Failed to load NPM plugin', { package: config.package, error: error.message });
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Load plugin from remote URL
|
|
*/
|
|
async function loadRemotePlugin(config) {
|
|
try {
|
|
const response = await fetch(config.url);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
const content = await response.text();
|
|
// Verify checksum if provided
|
|
if (config.checksum) {
|
|
const hash = await computeHash(content);
|
|
if (hash !== config.checksum) {
|
|
throw new Error('Checksum mismatch - plugin may be compromised');
|
|
}
|
|
}
|
|
// Parse plugin definition (JSON format)
|
|
const pluginDef = JSON.parse(content);
|
|
const plugin = {
|
|
name: pluginDef.name || 'remote-plugin',
|
|
version: pluginDef.version || '0.0.0',
|
|
source: `remote:${config.url}`,
|
|
tools: pluginDef.tools || [],
|
|
enabled: true,
|
|
loadedAt: Date.now()
|
|
};
|
|
loadedPlugins.set(plugin.name, plugin);
|
|
logger.info('Remote plugin loaded', { name: plugin.name, url: config.url });
|
|
return plugin;
|
|
}
|
|
catch (error) {
|
|
logger.error('Failed to load remote plugin', { url: config.url, error: error.message });
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Load inline plugin from configuration
|
|
*/
|
|
function loadInlinePlugin(config) {
|
|
const plugin = {
|
|
name: config.name,
|
|
version: '1.0.0',
|
|
source: 'inline',
|
|
tools: config.tools,
|
|
enabled: true,
|
|
loadedAt: Date.now()
|
|
};
|
|
loadedPlugins.set(plugin.name, plugin);
|
|
logger.info('Inline plugin loaded', { name: plugin.name, tools: config.tools.length });
|
|
return plugin;
|
|
}
|
|
/**
|
|
* Compute SHA-256 hash for checksum verification
|
|
*/
|
|
async function computeHash(content) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(content);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
/**
|
|
* Get all loaded plugins
|
|
*/
|
|
export function getLoadedPlugins() {
|
|
return Array.from(loadedPlugins.values());
|
|
}
|
|
/**
|
|
* Get plugin by name
|
|
*/
|
|
export function getPlugin(name) {
|
|
return loadedPlugins.get(name) || null;
|
|
}
|
|
/**
|
|
* Enable/disable a plugin
|
|
*/
|
|
export function setPluginEnabled(name, enabled) {
|
|
const plugin = loadedPlugins.get(name);
|
|
if (!plugin)
|
|
return false;
|
|
plugin.enabled = enabled;
|
|
logger.info('Plugin state changed', { name, enabled });
|
|
return true;
|
|
}
|
|
/**
|
|
* Unload a plugin
|
|
*/
|
|
export function unloadPlugin(name) {
|
|
const existed = loadedPlugins.delete(name);
|
|
if (existed) {
|
|
logger.info('Plugin unloaded', { name });
|
|
}
|
|
return existed;
|
|
}
|
|
/**
|
|
* Get all tools from enabled plugins
|
|
*/
|
|
export function getAllPluginTools() {
|
|
const tools = [];
|
|
for (const plugin of loadedPlugins.values()) {
|
|
if (plugin.enabled) {
|
|
tools.push(...plugin.tools);
|
|
}
|
|
}
|
|
return tools;
|
|
}
|
|
/**
|
|
* Execute a plugin tool
|
|
*/
|
|
export async function executePluginTool(toolName, input) {
|
|
for (const plugin of loadedPlugins.values()) {
|
|
if (!plugin.enabled)
|
|
continue;
|
|
const tool = plugin.tools.find(t => t.name === toolName);
|
|
if (tool) {
|
|
logger.info('Executing plugin tool', { plugin: plugin.name, tool: toolName });
|
|
return tool.handler(input);
|
|
}
|
|
}
|
|
throw new Error(`Plugin tool not found: ${toolName}`);
|
|
}
|
|
/**
|
|
* Load plugins from SDK configuration
|
|
*/
|
|
export async function loadPluginsFromConfig(configs) {
|
|
const loaded = [];
|
|
for (const config of configs) {
|
|
const plugin = await loadPlugin(config);
|
|
if (plugin) {
|
|
loaded.push(plugin);
|
|
}
|
|
}
|
|
logger.info('Plugins loaded from config', { total: loaded.length });
|
|
return loaded;
|
|
}
|
|
/**
|
|
* Get plugin configuration for SDK query options
|
|
*/
|
|
export function getPluginsForSdk() {
|
|
const plugins = [];
|
|
for (const plugin of loadedPlugins.values()) {
|
|
if (!plugin.enabled)
|
|
continue;
|
|
if (plugin.source.startsWith('local:')) {
|
|
plugins.push({
|
|
type: 'local',
|
|
path: plugin.source.replace('local:', '')
|
|
});
|
|
}
|
|
else if (plugin.source.startsWith('npm:')) {
|
|
plugins.push({
|
|
type: 'npm',
|
|
package: plugin.source.replace('npm:', '')
|
|
});
|
|
}
|
|
}
|
|
return plugins;
|
|
}
|
|
/**
|
|
* Create an inline plugin helper
|
|
*/
|
|
export function createPlugin(name, tools) {
|
|
return loadInlinePlugin({ type: 'inline', name, tools });
|
|
}
|
|
/**
|
|
* Plugin tool builder for type-safe tool creation
|
|
*/
|
|
export function defineTool(config) {
|
|
return {
|
|
name: config.name,
|
|
description: config.description,
|
|
inputSchema: config.inputSchema,
|
|
handler: config.handler
|
|
};
|
|
}
|
|
//# sourceMappingURL=plugins.js.map
|