From 7926dbffd7a4fe3a6ae287e00d7e43e0090a38d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 08:10:08 +0000 Subject: [PATCH 1/2] feat: add OpenClaw plugin system support (tool, memory, channel, provider) Implements full OpenClaw plugin lifecycle integration into Lobu: - Plugin types and interfaces in packages/core (PluginManifest, LoadedPlugin, etc.) - Plugin loader service in gateway with shim OpenClawPluginApi for discovery/registration - Plugin bridge in worker converting tool/memory/provider plugins to pi-coding-agent format - Channel adapter wrapping OpenClaw channel plugins as Lobu PlatformAdapters - Plugin config wired through agent settings, API routes, and all message handlers https://claude.ai/code/session_01TnnWVhe45TgmLSSbKBzYx3 --- packages/core/src/index.ts | 17 + packages/core/src/plugin-types.ts | 175 ++++++ .../src/auth/settings/agent-settings-store.ts | 3 + packages/gateway/src/cli/gateway.ts | 46 ++ .../gateway/src/plugins/channel-adapter.ts | 255 +++++++++ packages/gateway/src/plugins/plugin-loader.ts | 525 ++++++++++++++++++ .../gateway/src/routes/public/agent-config.ts | 17 + packages/gateway/src/slack/events/messages.ts | 3 + .../src/telegram/events/message-handler.ts | 3 + .../src/whatsapp/events/message-handler.ts | 3 + packages/worker/src/openclaw/plugin-bridge.ts | 278 ++++++++++ packages/worker/src/openclaw/worker.ts | 213 ++++++- 12 files changed, 1536 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/plugin-types.ts create mode 100644 packages/gateway/src/plugins/channel-adapter.ts create mode 100644 packages/gateway/src/plugins/plugin-loader.ts create mode 100644 packages/worker/src/openclaw/plugin-bridge.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 55d47cceb..3ed5b9ad0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,6 +64,23 @@ export type { UserSuggestion, } from "./types"; +// Plugin types +export type { + LoadedPlugin, + OpenClawChannelDef, + OpenClawChannelOutbound, + OpenClawMemoryDef, + OpenClawMemoryResult, + OpenClawProviderDef, + OpenClawServiceDef, + OpenClawToolDef, + PluginConfig, + PluginManifest, + PluginRegistrations, + PluginSlot, + PluginsConfig, +} from "./plugin-types"; + // Utilities export * from "./utils/encryption"; export * from "./utils/env"; diff --git a/packages/core/src/plugin-types.ts b/packages/core/src/plugin-types.ts new file mode 100644 index 000000000..101b4bb40 --- /dev/null +++ b/packages/core/src/plugin-types.ts @@ -0,0 +1,175 @@ +/** + * OpenClaw Plugin System Types + * + * Defines the interfaces for loading, configuring, and bridging OpenClaw plugins + * into Lobu's architecture. Supports all four plugin slot types: + * - tool: Agent capabilities (tools available during turns) + * - memory: Context recall/save backends (exclusive slot - one active at a time) + * - channel: Messaging platform integrations + * - provider: AI model inference backends + */ + +// ============================================================================ +// Plugin Configuration (stored in AgentSettings) +// ============================================================================ + +/** Slot types supported by OpenClaw plugins */ +export type PluginSlot = "tool" | "memory" | "channel" | "provider"; + +/** Individual plugin configuration in agent settings */ +export interface PluginConfig { + /** npm package name or local path (e.g., "@openclaw/msteams", "./extensions/matrix") */ + source: string; + /** Whether this plugin is currently enabled */ + enabled: boolean; + /** Plugin-specific configuration values (validated against plugin's configSchema) */ + config?: Record; +} + +/** Plugins configuration for agent settings */ +export interface PluginsConfig { + /** Installed plugins keyed by plugin ID */ + plugins: Record; + /** Exclusive slot assignments (e.g., { memory: "memory-core" }) */ + slots?: Record; +} + +// ============================================================================ +// Plugin Manifest (read from plugin package) +// ============================================================================ + +/** Plugin manifest from openclaw.plugin.json or package.json */ +export interface PluginManifest { + id: string; + name?: string; + description?: string; + kind?: PluginSlot; + configSchema?: Record; + channels?: string[]; + providers?: string[]; + skills?: string[]; +} + +// ============================================================================ +// Plugin API (shim provided to plugins during registration) +// ============================================================================ + +/** Tool definition as registered by OpenClaw plugins */ +export interface OpenClawToolDef { + name: string; + description: string; + parameters: unknown; // TypeBox schema + execute: ( + toolCallId: string, + params: unknown, + signal?: AbortSignal + ) => Promise<{ + content: Array<{ type: string; text: string }>; + details?: unknown; + }>; +} + +/** Channel plugin outbound interface */ +export interface OpenClawChannelOutbound { + deliveryMode: "direct" | "buffered"; + sendText: (params: { + text: string; + [key: string]: unknown; + }) => Promise<{ ok: boolean }>; + sendMedia?: (params: { [key: string]: unknown }) => Promise<{ ok: boolean }>; +} + +/** Channel plugin as registered by OpenClaw plugins */ +export interface OpenClawChannelDef { + id: string; + meta: { + id: string; + label: string; + docsPath?: string; + blurb?: string; + aliases?: string[]; + }; + capabilities: { + chatTypes: Array<"direct" | "group" | "thread" | "channel">; + reactions?: boolean; + threads?: boolean; + media?: boolean; + }; + config: { + listAccountIds: (cfg: unknown) => string[]; + resolveAccount: (cfg: unknown, accountId?: string) => unknown; + }; + outbound: OpenClawChannelOutbound; + startAccount?: (accountId: string) => Promise; + stopAccount?: (accountId: string) => Promise; +} + +/** Memory plugin interface */ +export interface OpenClawMemoryDef { + indexChunk?: ( + path: string, + content: string, + metadata?: unknown + ) => Promise; + search?: ( + query: string, + options?: unknown + ) => Promise; + recall?: (query: string) => Promise; + save?: (content: string, metadata?: unknown) => Promise; +} + +export interface OpenClawMemoryResult { + text: string; + path?: string; + score?: number; + metadata?: unknown; +} + +/** Provider plugin interface */ +export interface OpenClawProviderDef { + id: string; + models?: Array<{ + id: string; + name: string; + api?: string; + contextWindow?: number; + maxTokens?: number; + }>; + stream?: (messages: unknown[], options?: unknown) => AsyncIterable; + validateAuth?: () => Promise; +} + +/** Service registration (background services, HTTP endpoints) */ +export interface OpenClawServiceDef { + type: "http" | "background"; + path?: string; + handler?: (req: unknown, res: unknown) => Promise; + start?: () => Promise; + stop?: () => Promise; +} + +/** + * Collected registrations from an OpenClaw plugin. + * The plugin loader calls register(api) and captures all registrations here. + */ +export interface PluginRegistrations { + id: string; + slot?: PluginSlot; + tools: OpenClawToolDef[]; + channels: OpenClawChannelDef[]; + memory: OpenClawMemoryDef | null; + provider: OpenClawProviderDef | null; + services: OpenClawServiceDef[]; + commands: Map unknown>; + gatewayMethods: Map unknown>; +} + +/** + * A fully loaded plugin with its manifest and registrations. + */ +export interface LoadedPlugin { + manifest: PluginManifest; + config: PluginConfig; + registrations: PluginRegistrations; +} diff --git a/packages/gateway/src/auth/settings/agent-settings-store.ts b/packages/gateway/src/auth/settings/agent-settings-store.ts index 1f0695e7b..601e42e22 100644 --- a/packages/gateway/src/auth/settings/agent-settings-store.ts +++ b/packages/gateway/src/auth/settings/agent-settings-store.ts @@ -4,6 +4,7 @@ import { type McpServerConfig, type NetworkConfig, type NixConfig, + type PluginsConfig, type SkillsConfig, type ToolsConfig, } from "@lobu/core"; @@ -36,6 +37,8 @@ export interface AgentSettings { skillsConfig?: SkillsConfig; /** Tool permission configuration - allowed/denied tools */ toolsConfig?: ToolsConfig; + /** OpenClaw plugins configuration */ + pluginsConfig?: PluginsConfig; /** Enable verbose logging (show tool calls, reasoning, etc.) */ verboseLogging?: boolean; /** Connected GitHub user info */ diff --git a/packages/gateway/src/cli/gateway.ts b/packages/gateway/src/cli/gateway.ts index 54ca66c20..276f713e7 100644 --- a/packages/gateway/src/cli/gateway.ts +++ b/packages/gateway/src/cli/gateway.ts @@ -855,6 +855,52 @@ export async function startGateway( gateway.registerPlatform(apiPlatform); logger.info("API platform registered"); + // Load and register OpenClaw channel plugins + try { + const { loadPlugins, getPluginsBySlot } = await import( + "../plugins/plugin-loader" + ); + const { createChannelAdapters } = await import( + "../plugins/channel-adapter" + ); + + // Load global plugin config from env or default + const globalPluginsConfigPath = process.env.LOBU_PLUGINS_CONFIG; + if (globalPluginsConfigPath) { + const { readFile } = await import("node:fs/promises"); + const configContent = await readFile(globalPluginsConfigPath, "utf-8"); + const pluginsConfig = JSON.parse(configContent); + + const plugins = await loadPlugins(pluginsConfig, { + baseDir: process.cwd(), + extensionDirs: process.env.LOBU_EXTENSIONS_DIR + ? [process.env.LOBU_EXTENSIONS_DIR] + : [], + }); + + // Register channel plugins as platform adapters + const channelPlugins = getPluginsBySlot(plugins, "channel"); + if (channelPlugins.length > 0) { + const adapters = createChannelAdapters(channelPlugins); + for (const adapter of adapters) { + gateway.registerPlatform(adapter); + logger.info(`OpenClaw channel plugin registered: ${adapter.name}`); + } + } + + logger.info( + `OpenClaw plugins loaded: ${plugins.length} total, ${channelPlugins.length} channel adapters` + ); + } + } catch (error) { + logger.debug( + "OpenClaw plugins: none loaded (this is normal if not configured)", + { + error: error instanceof Error ? error.message : String(error), + } + ); + } + // Start gateway await gateway.start(); logger.info("Gateway started"); diff --git a/packages/gateway/src/plugins/channel-adapter.ts b/packages/gateway/src/plugins/channel-adapter.ts new file mode 100644 index 000000000..84fdc4b9a --- /dev/null +++ b/packages/gateway/src/plugins/channel-adapter.ts @@ -0,0 +1,255 @@ +/** + * OpenClaw Channel Plugin → Lobu PlatformAdapter + * + * Wraps an OpenClaw channel plugin as a Lobu PlatformAdapter so it can + * be registered with the gateway and participate in message routing. + */ + +import { + createLogger, + type LoadedPlugin, + type OpenClawChannelDef, + type ThreadResponsePayload, +} from "@lobu/core"; +import type { CoreServices, PlatformAdapter } from "../platform"; +import type { ResponseRenderer } from "../platform/response-renderer"; + +const logger = createLogger("channel-adapter"); + +/** + * ResponseRenderer for OpenClaw channel plugins. + * Converts Lobu ThreadResponsePayload into channel outbound calls. + */ +class PluginChannelResponseRenderer implements ResponseRenderer { + private channelDef: OpenClawChannelDef; + private messageBuffers = new Map(); + + constructor(channelDef: OpenClawChannelDef) { + this.channelDef = channelDef; + } + + async handleDelta( + payload: ThreadResponsePayload, + _sessionKey: string + ): Promise { + const key = `${payload.channelId}:${payload.conversationId}`; + + if (payload.isFullReplacement) { + this.messageBuffers.set(key, payload.delta || ""); + } else { + const existing = this.messageBuffers.get(key) || ""; + this.messageBuffers.set(key, existing + (payload.delta || "")); + } + + // Deliver current buffer + const text = this.messageBuffers.get(key) || ""; + try { + await this.channelDef.outbound.sendText({ + text, + channelId: payload.channelId, + conversationId: payload.conversationId, + }); + } catch (error) { + logger.error(`[channel:${this.channelDef.id}] Failed to send delta:`, { + error, + }); + } + + return null; + } + + async handleCompletion( + payload: ThreadResponsePayload, + _sessionKey: string + ): Promise { + const key = `${payload.channelId}:${payload.conversationId}`; + const finalText = this.messageBuffers.get(key); + + if (finalText) { + try { + await this.channelDef.outbound.sendText({ + text: finalText, + channelId: payload.channelId, + conversationId: payload.conversationId, + isFinal: true, + }); + } catch (error) { + logger.error( + `[channel:${this.channelDef.id}] Failed to send completion:`, + { error } + ); + } + } + + this.messageBuffers.delete(key); + } + + async handleError( + payload: ThreadResponsePayload, + _sessionKey: string + ): Promise { + try { + await this.channelDef.outbound.sendText({ + text: `Error: ${payload.error || "Unknown error"}`, + channelId: payload.channelId, + conversationId: payload.conversationId, + }); + } catch (error) { + logger.error(`[channel:${this.channelDef.id}] Failed to send error:`, { + error, + }); + } + + const key = `${payload.channelId}:${payload.conversationId}`; + this.messageBuffers.delete(key); + } + + async handleStatusUpdate(_payload: ThreadResponsePayload): Promise { + // Status updates (heartbeats) -- plugins can show typing indicators here + logger.debug(`[channel:${this.channelDef.id}] Status update (no-op)`); + } + + async handleEphemeral(payload: ThreadResponsePayload): Promise { + try { + await this.channelDef.outbound.sendText({ + text: payload.content || "", + channelId: payload.channelId, + conversationId: payload.conversationId, + ephemeral: true, + }); + } catch (error) { + logger.error( + `[channel:${this.channelDef.id}] Failed to send ephemeral:`, + { error } + ); + } + } +} + +/** + * Wraps an OpenClaw channel plugin as a Lobu PlatformAdapter. + */ +export class PluginChannelAdapter implements PlatformAdapter { + readonly name: string; + private channelDef: OpenClawChannelDef; + private plugin: LoadedPlugin; + private responseRenderer: PluginChannelResponseRenderer; + private running = false; + + constructor(plugin: LoadedPlugin, channelDef: OpenClawChannelDef) { + this.plugin = plugin; + this.channelDef = channelDef; + this.name = channelDef.id; + this.responseRenderer = new PluginChannelResponseRenderer(channelDef); + } + + async initialize(_services: CoreServices): Promise { + logger.info(`[channel:${this.name}] Initialized with core services`); + } + + async start(): Promise { + if (this.channelDef.startAccount) { + const accountIds = this.channelDef.config.listAccountIds( + this.plugin.config.config || {} + ); + for (const accountId of accountIds) { + try { + await this.channelDef.startAccount(accountId); + logger.info(`[channel:${this.name}] Started account: ${accountId}`); + } catch (error) { + logger.error( + `[channel:${this.name}] Failed to start account ${accountId}:`, + { error } + ); + } + } + } + this.running = true; + logger.info(`[channel:${this.name}] Platform started`); + } + + async stop(): Promise { + if (this.channelDef.stopAccount) { + const accountIds = this.channelDef.config.listAccountIds( + this.plugin.config.config || {} + ); + for (const accountId of accountIds) { + try { + await this.channelDef.stopAccount(accountId); + } catch (error) { + logger.error( + `[channel:${this.name}] Failed to stop account ${accountId}:`, + { error } + ); + } + } + } + this.running = false; + logger.info(`[channel:${this.name}] Platform stopped`); + } + + isHealthy(): boolean { + return this.running; + } + + getResponseRenderer(): ResponseRenderer | undefined { + return this.responseRenderer; + } + + buildDeploymentMetadata( + conversationId: string, + channelId: string, + _platformMetadata: Record + ): Record { + return { + platform: this.name, + channelId, + conversationId, + }; + } + + getDisplayInfo() { + return { + name: this.channelDef.meta.label, + icon: "🔌", + }; + } + + async sendMessage( + _token: string, + message: string, + options: { + agentId: string; + channelId: string; + conversationId?: string; + teamId: string; + } + ): Promise<{ messageId: string }> { + await this.channelDef.outbound.sendText({ + text: message, + channelId: options.channelId, + conversationId: options.conversationId, + }); + return { messageId: `plugin-${Date.now()}` }; + } +} + +/** + * Create PlatformAdapters for all channel plugins. + */ +export function createChannelAdapters( + plugins: LoadedPlugin[] +): PluginChannelAdapter[] { + const adapters: PluginChannelAdapter[] = []; + + for (const plugin of plugins) { + for (const channelDef of plugin.registrations.channels) { + adapters.push(new PluginChannelAdapter(plugin, channelDef)); + logger.info( + `Created channel adapter: ${channelDef.id} (${channelDef.meta.label})` + ); + } + } + + return adapters; +} diff --git a/packages/gateway/src/plugins/plugin-loader.ts b/packages/gateway/src/plugins/plugin-loader.ts new file mode 100644 index 000000000..dd5ec677a --- /dev/null +++ b/packages/gateway/src/plugins/plugin-loader.ts @@ -0,0 +1,525 @@ +/** + * OpenClaw Plugin Loader + * + * Discovers, validates, and loads OpenClaw plugins. Provides a shim + * OpenClawPluginApi that captures all registrations (tools, channels, + * memory, providers, services) so Lobu can route them to the appropriate + * subsystems. + * + * Discovery sources: + * - node_modules/@openclaw/* + * - node_modules/@* /openclaw-* (community namespace) + * - Configured local paths (extensions/ directory) + * - Per-agent plugin config from settings + */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { + createLogger, + type LoadedPlugin, + type OpenClawChannelDef, + type OpenClawMemoryDef, + type OpenClawProviderDef, + type OpenClawServiceDef, + type OpenClawToolDef, + type PluginConfig, + type PluginManifest, + type PluginRegistrations, + type PluginsConfig, +} from "@lobu/core"; + +const logger = createLogger("plugin-loader"); + +// ============================================================================ +// Plugin API Shim +// ============================================================================ + +/** + * Creates a shim OpenClawPluginApi that captures all registrations + * from a plugin's register() or init() call. + */ +function createPluginApiShim( + pluginId: string, + pluginConfig: Record +): { api: Record; registrations: PluginRegistrations } { + const registrations: PluginRegistrations = { + id: pluginId, + tools: [], + channels: [], + memory: null, + provider: null, + services: [], + commands: new Map(), + gatewayMethods: new Map(), + }; + + const api = { + logger: { + info: (...args: unknown[]) => + logger.info(`[plugin:${pluginId}]`, ...args), + warn: (...args: unknown[]) => + logger.warn(`[plugin:${pluginId}]`, ...args), + error: (...args: unknown[]) => + logger.error(`[plugin:${pluginId}]`, ...args), + debug: (...args: unknown[]) => + logger.debug(`[plugin:${pluginId}]`, ...args), + }, + + config: pluginConfig, + + runtime: { + // Stub runtime utilities -- plugins can check capabilities + tts: null, + stt: null, + }, + + registerTool(definition: OpenClawToolDef) { + logger.info(`Plugin ${pluginId} registered tool: ${definition.name}`); + registrations.tools.push(definition); + }, + + registerChannel(opts: { plugin: OpenClawChannelDef }) { + logger.info(`Plugin ${pluginId} registered channel: ${opts.plugin.id}`); + registrations.channels.push(opts.plugin); + registrations.slot = "channel"; + }, + + registerProvider(config: OpenClawProviderDef) { + logger.info(`Plugin ${pluginId} registered provider: ${config.id}`); + registrations.provider = config; + registrations.slot = "provider"; + }, + + registerService(config: OpenClawServiceDef) { + logger.info(`Plugin ${pluginId} registered service: ${config.type}`); + registrations.services.push(config); + }, + + registerGatewayMethod( + name: string, + handler: (...args: unknown[]) => unknown + ) { + logger.info(`Plugin ${pluginId} registered gateway method: ${name}`); + registrations.gatewayMethods.set(name, handler); + }, + + registerCommand(options: { + name: string; + handler: (...args: unknown[]) => unknown; + }) { + logger.info(`Plugin ${pluginId} registered command: ${options.name}`); + registrations.commands.set(options.name, options.handler); + }, + + registerCli() { + logger.debug( + `Plugin ${pluginId} registered CLI (ignored in Lobu context)` + ); + }, + + // Event subscription (no-op in shim -- lifecycle hooks handled separately) + on(event: string) { + logger.debug(`Plugin ${pluginId} subscribed to event: ${event}`); + }, + }; + + return { api, registrations }; +} + +// ============================================================================ +// Plugin Discovery +// ============================================================================ + +interface DiscoveredPlugin { + packagePath: string; + packageJson: Record; + manifest?: PluginManifest; + entryPoints: string[]; +} + +/** + * Read and parse openclaw.plugin.json if it exists. + */ +async function readPluginManifest( + dir: string +): Promise { + try { + const manifestPath = path.join(dir, "openclaw.plugin.json"); + const content = await fs.readFile(manifestPath, "utf-8"); + return JSON.parse(content) as PluginManifest; + } catch { + return undefined; + } +} + +/** + * Discover OpenClaw plugins from a node_modules directory. + * Scans @openclaw/_ and @_/openclaw-_ packages (community namespace). + */ +async function discoverFromNodeModules( + baseDir: string +): Promise { + const nodeModules = path.join(baseDir, "node_modules"); + const discovered: DiscoveredPlugin[] = []; + + try { + await fs.stat(nodeModules); + } catch { + return discovered; + } + + // Scan @openclaw/* packages + const openclawDir = path.join(nodeModules, "@openclaw"); + try { + const entries = await fs.readdir(openclawDir); + for (const entry of entries) { + const plugin = await tryLoadPluginPackage(path.join(openclawDir, entry)); + if (plugin) discovered.push(plugin); + } + } catch { + // @openclaw directory doesn't exist -- that's fine + } + + // Scan @*/openclaw-* packages (community namespace) + try { + const scopes = await fs.readdir(nodeModules); + for (const scope of scopes) { + if (!scope.startsWith("@") || scope === "@openclaw") continue; + try { + const scopeDir = path.join(nodeModules, scope); + const packages = await fs.readdir(scopeDir); + for (const pkg of packages) { + if (!pkg.startsWith("openclaw-")) continue; + const plugin = await tryLoadPluginPackage(path.join(scopeDir, pkg)); + if (plugin) discovered.push(plugin); + } + } catch { + // skip unreadable scope dir + } + } + } catch { + // node_modules not readable + } + + return discovered; +} + +/** + * Discover plugins from a local extensions directory. + */ +async function discoverFromExtensions( + extensionsDir: string +): Promise { + const discovered: DiscoveredPlugin[] = []; + + try { + const entries = await fs.readdir(extensionsDir); + for (const entry of entries) { + const plugin = await tryLoadPluginPackage( + path.join(extensionsDir, entry) + ); + if (plugin) discovered.push(plugin); + } + } catch { + // Extensions directory doesn't exist + } + + return discovered; +} + +/** + * Try to load a plugin from a directory. + * Returns null if not a valid OpenClaw plugin. + */ +async function tryLoadPluginPackage( + dir: string +): Promise { + try { + const pkgPath = path.join(dir, "package.json"); + const content = await fs.readFile(pkgPath, "utf-8"); + const packageJson = JSON.parse(content); + + // Check for openclaw.extensions field + const openclawConfig = packageJson.openclaw; + if ( + !openclawConfig?.extensions || + !Array.isArray(openclawConfig.extensions) + ) { + return null; + } + + const manifest = await readPluginManifest(dir); + + return { + packagePath: dir, + packageJson, + manifest, + entryPoints: openclawConfig.extensions as string[], + }; + } catch { + return null; + } +} + +// ============================================================================ +// Plugin Loading +// ============================================================================ + +/** + * Load a single plugin by importing its entry points and calling register/init. + */ +async function loadPlugin( + discovered: DiscoveredPlugin, + pluginConfig: PluginConfig +): Promise { + const pluginId = + discovered.manifest?.id || + (discovered.packageJson.name as string) || + path.basename(discovered.packagePath); + + logger.info(`Loading plugin: ${pluginId} from ${discovered.packagePath}`); + + const { api, registrations } = createPluginApiShim( + pluginId, + pluginConfig.config || {} + ); + + for (const entryPoint of discovered.entryPoints) { + const entryPath = path.resolve(discovered.packagePath, entryPoint); + + try { + const module = await import(entryPath); + const exported = module.default || module; + + if (typeof exported === "function") { + // Function form: register(api) + await exported(api); + } else if (typeof exported === "object" && exported !== null) { + if (typeof exported.register === "function") { + // Object form with register method + await exported.register(api); + } else if (typeof exported.init === "function") { + // PluginDefinition form with init method + const result = await exported.init(pluginConfig.config || {}, { + logger: api.logger, + configDir: process.env.HOME || "/tmp", + workspaceDir: process.cwd(), + rpc: {}, // Stub -- will be wired at runtime + }); + + // Capture slot-specific registrations from init result + if (exported.slot === "memory" || exported.kind === "memory") { + registrations.memory = result as OpenClawMemoryDef; + registrations.slot = "memory"; + } else if ( + exported.slot === "provider" || + exported.kind === "provider" + ) { + registrations.provider = result as OpenClawProviderDef; + registrations.slot = "provider"; + } else if ( + exported.slot === "channel" || + exported.kind === "channel" + ) { + // Channel plugins return { start, stop, send } from init + if (result && typeof result === "object") { + const channelResult = result as { + start?: () => Promise; + stop?: () => Promise; + send?: (envelope: unknown) => Promise; + }; + registrations.channels.push({ + id: exported.id || pluginId, + meta: { + id: exported.id || pluginId, + label: exported.metadata?.name || pluginId, + docsPath: "", + }, + capabilities: { chatTypes: ["direct", "group"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + }, + outbound: { + deliveryMode: "direct", + sendText: async (params) => { + if (channelResult.send) { + await channelResult.send(params); + } + return { ok: true }; + }, + }, + startAccount: channelResult.start + ? async () => channelResult.start!() + : undefined, + stopAccount: channelResult.stop + ? async () => channelResult.stop!() + : undefined, + }); + registrations.slot = "channel"; + } + } else if (exported.slot === "tool") { + // Tool plugins return tool definitions from init + if (Array.isArray(result)) { + for (const tool of result) { + registrations.tools.push(tool as OpenClawToolDef); + } + } + registrations.slot = "tool"; + } + } + } + } catch (error) { + logger.error(`Failed to load plugin entry point ${entryPath}:`, { + error, + }); + return null; + } + } + + // Infer slot from manifest if not set during registration + if (!registrations.slot && discovered.manifest?.kind) { + registrations.slot = discovered.manifest.kind; + } + + const manifest: PluginManifest = discovered.manifest || { + id: pluginId, + name: (discovered.packageJson.name as string) || pluginId, + description: (discovered.packageJson.description as string) || undefined, + }; + + logger.info( + `Plugin ${pluginId} loaded: ${registrations.tools.length} tools, ${registrations.channels.length} channels, memory=${!!registrations.memory}, provider=${!!registrations.provider}` + ); + + return { + manifest, + config: pluginConfig, + registrations, + }; +} + +// ============================================================================ +// Public API +// ============================================================================ + +export interface PluginLoaderOptions { + /** Base directory for node_modules discovery */ + baseDir?: string; + /** Additional directories to scan for plugins */ + extensionDirs?: string[]; +} + +/** + * Discover all available OpenClaw plugins (without loading them). + * Returns plugin IDs and metadata for configuration UI. + */ +export async function discoverPlugins( + options?: PluginLoaderOptions +): Promise { + const baseDir = options?.baseDir || process.cwd(); + const discovered: DiscoveredPlugin[] = []; + + // Discover from node_modules + const fromNpm = await discoverFromNodeModules(baseDir); + discovered.push(...fromNpm); + + // Discover from extension directories + for (const dir of options?.extensionDirs || []) { + const fromDir = await discoverFromExtensions(dir); + discovered.push(...fromDir); + } + + logger.info(`Discovered ${discovered.length} OpenClaw plugins`); + return discovered; +} + +/** + * Load all enabled plugins from a PluginsConfig. + * Returns loaded plugins organized by slot type. + */ +export async function loadPlugins( + pluginsConfig: PluginsConfig, + options?: PluginLoaderOptions +): Promise { + const discovered = await discoverPlugins(options); + const loaded: LoadedPlugin[] = []; + + for (const [pluginId, config] of Object.entries(pluginsConfig.plugins)) { + if (!config.enabled) { + logger.debug(`Skipping disabled plugin: ${pluginId}`); + continue; + } + + // Find the discovered plugin matching this config + const found = discovered.find((d) => { + const id = + d.manifest?.id || d.packageJson.name || path.basename(d.packagePath); + return id === pluginId || id === config.source; + }); + + if (!found) { + // Try loading directly from source path + if (config.source) { + const directPlugin = await tryLoadPluginPackage( + path.resolve(options?.baseDir || process.cwd(), config.source) + ); + if (directPlugin) { + const plugin = await loadPlugin(directPlugin, config); + if (plugin) loaded.push(plugin); + continue; + } + } + + logger.warn(`Plugin not found: ${pluginId} (source: ${config.source})`); + continue; + } + + const plugin = await loadPlugin(found, config); + if (plugin) { + // Check exclusive slot constraints + if (pluginsConfig.slots && plugin.registrations.slot) { + const slotAssignment = pluginsConfig.slots[plugin.registrations.slot]; + if (slotAssignment && slotAssignment !== pluginId) { + logger.info( + `Skipping plugin ${pluginId} -- slot ${plugin.registrations.slot} assigned to ${slotAssignment}` + ); + continue; + } + } + + loaded.push(plugin); + } + } + + logger.info(`Loaded ${loaded.length} plugins`); + return loaded; +} + +/** + * Filter loaded plugins by slot type. + */ +export function getPluginsBySlot( + plugins: LoadedPlugin[], + slot: string +): LoadedPlugin[] { + return plugins.filter((p) => p.registrations.slot === slot); +} + +/** + * Get all tool definitions from loaded plugins. + */ +export function getPluginTools(plugins: LoadedPlugin[]): OpenClawToolDef[] { + return plugins.flatMap((p) => p.registrations.tools); +} + +/** + * Get the active memory plugin (only one allowed). + */ +export function getActiveMemoryPlugin( + plugins: LoadedPlugin[] +): LoadedPlugin | undefined { + const memoryPlugins = getPluginsBySlot(plugins, "memory"); + return memoryPlugins[0]; // First one wins (slot assignment handled during loading) +} diff --git a/packages/gateway/src/routes/public/agent-config.ts b/packages/gateway/src/routes/public/agent-config.ts index 6caf5ad09..7fa205e1b 100644 --- a/packages/gateway/src/routes/public/agent-config.ts +++ b/packages/gateway/src/routes/public/agent-config.ts @@ -115,6 +115,19 @@ const updateConfigRoute = createRoute({ ), }) .optional(), + pluginsConfig: z + .object({ + plugins: z.record( + z.string(), + z.object({ + source: z.string(), + enabled: z.boolean(), + config: z.record(z.string(), z.any()).optional(), + }) + ), + slots: z.record(z.string(), z.string()).optional(), + }) + .optional(), verboseLogging: z.boolean().optional(), githubUser: z .null() @@ -456,6 +469,10 @@ function validateSettings( settings.skillsConfig = input.skillsConfig; } + if (input.pluginsConfig) { + settings.pluginsConfig = input.pluginsConfig; + } + if (typeof input.verboseLogging === "boolean") { settings.verboseLogging = input.verboseLogging; } diff --git a/packages/gateway/src/slack/events/messages.ts b/packages/gateway/src/slack/events/messages.ts index 6b0671e34..464f0c5ad 100644 --- a/packages/gateway/src/slack/events/messages.ts +++ b/packages/gateway/src/slack/events/messages.ts @@ -253,6 +253,9 @@ export class MessageHandler { if (settings.verboseLogging !== undefined) { mergedOptions.verboseLogging = settings.verboseLogging; } + if (settings.pluginsConfig) { + mergedOptions.pluginsConfig = settings.pluginsConfig; + } return mergedOptions; } diff --git a/packages/gateway/src/telegram/events/message-handler.ts b/packages/gateway/src/telegram/events/message-handler.ts index 0e24cb020..dc16b73c5 100644 --- a/packages/gateway/src/telegram/events/message-handler.ts +++ b/packages/gateway/src/telegram/events/message-handler.ts @@ -139,6 +139,9 @@ export class TelegramMessageHandler { if (settings.verboseLogging !== undefined) { mergedOptions.verboseLogging = settings.verboseLogging; } + if (settings.pluginsConfig) { + mergedOptions.pluginsConfig = settings.pluginsConfig; + } return mergedOptions; } diff --git a/packages/gateway/src/whatsapp/events/message-handler.ts b/packages/gateway/src/whatsapp/events/message-handler.ts index 8731c92c5..22316e1bf 100644 --- a/packages/gateway/src/whatsapp/events/message-handler.ts +++ b/packages/gateway/src/whatsapp/events/message-handler.ts @@ -214,6 +214,9 @@ export class WhatsAppMessageHandler { if (settings.verboseLogging !== undefined) { mergedOptions.verboseLogging = settings.verboseLogging; } + if (settings.pluginsConfig) { + mergedOptions.pluginsConfig = settings.pluginsConfig; + } return mergedOptions; } diff --git a/packages/worker/src/openclaw/plugin-bridge.ts b/packages/worker/src/openclaw/plugin-bridge.ts new file mode 100644 index 000000000..5dea4a542 --- /dev/null +++ b/packages/worker/src/openclaw/plugin-bridge.ts @@ -0,0 +1,278 @@ +/** + * OpenClaw Plugin Bridge + * + * Bridges OpenClaw plugin registrations into the pi-coding-agent runtime: + * - Tool plugins → ToolDefinition objects injected into createAgentSession + * - Memory plugins → before_agent_start (recall) and agent_end (save) hooks + * - Provider plugins → model registration (future) + * + * Channel plugins are handled gateway-side (see gateway/src/plugins/channel-adapter.ts). + */ + +import { + createLogger, + type LoadedPlugin, + type OpenClawToolDef, +} from "@lobu/core"; +import type { AgentToolResult } from "@mariozechner/pi-agent-core"; +import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; + +const logger = createLogger("plugin-bridge"); + +// ============================================================================ +// Tool Plugin Bridge +// ============================================================================ + +/** + * Convert an OpenClaw tool definition to pi-coding-agent ToolDefinition format. + */ +function bridgeToolDef( + pluginId: string, + tool: OpenClawToolDef +): ToolDefinition { + // Convert TypeBox schema or pass through + const parameters = tool.parameters || Type.Object({}); + + return { + name: tool.name, + label: tool.name, + description: tool.description || `Tool from plugin ${pluginId}`, + parameters: parameters as any, + execute: async ( + toolCallId: string, + params: unknown, + signal?: AbortSignal + ): Promise>> => { + try { + logger.info(`[plugin:${pluginId}] Executing tool: ${tool.name}`); + const result = await tool.execute(toolCallId, params, signal); + + // Normalize result to AgentToolResult format + const content = (result?.content || []).map((block) => { + if (block.type === "text") { + return { type: "text" as const, text: block.text }; + } + return { type: "text" as const, text: JSON.stringify(block) }; + }); + + if (content.length === 0) { + content.push({ + type: "text" as const, + text: "Tool executed successfully", + }); + } + + return { content, details: {} }; + } catch (error) { + logger.error(`[plugin:${pluginId}] Tool ${tool.name} failed:`, { + error, + }); + return { + content: [ + { + type: "text" as const, + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + details: {}, + }; + } + }, + }; +} + +/** + * Extract all tool definitions from loaded plugins. + * Returns ToolDefinition[] ready for createAgentSession({ customTools }). + */ +export function bridgePluginTools(plugins: LoadedPlugin[]): ToolDefinition[] { + const tools: ToolDefinition[] = []; + + for (const plugin of plugins) { + for (const toolDef of plugin.registrations.tools) { + tools.push(bridgeToolDef(plugin.manifest.id, toolDef)); + } + } + + if (tools.length > 0) { + logger.info( + `Bridged ${tools.length} plugin tools: ${tools.map((t) => t.name).join(", ")}` + ); + } + + return tools; +} + +// ============================================================================ +// Memory Plugin Bridge +// ============================================================================ + +/** + * Memory plugin lifecycle hooks that integrate with the agent session. + * Call recall() before prompting and save() after the agent responds. + */ +export interface MemoryHooks { + /** Recall relevant memories before agent processes a message */ + recall(query: string): Promise; + /** Save conversation context after agent responds */ + save(content: string, metadata?: Record): Promise; +} + +/** + * Create memory hooks from a loaded memory plugin. + * Returns null if no memory plugin is active. + */ +export function bridgeMemoryPlugin( + plugins: LoadedPlugin[] +): MemoryHooks | null { + const memoryPlugin = plugins.find((p) => p.registrations.slot === "memory"); + if (!memoryPlugin || !memoryPlugin.registrations.memory) { + return null; + } + + const pluginId = memoryPlugin.manifest.id; + const memory = memoryPlugin.registrations.memory; + + logger.info(`Memory plugin active: ${pluginId}`); + + return { + async recall(query: string): Promise { + try { + // Try the recall method first (simple API) + if (memory.recall) { + const result = await memory.recall(query); + logger.info( + `[memory:${pluginId}] Recalled ${result.length} chars for query` + ); + return result; + } + + // Fall back to search method (full API) + if (memory.search) { + const results = await memory.search(query, { + maxResults: 6, + minScore: 0.35, + }); + if (!results || results.length === 0) { + return ""; + } + + const formatted = results + .map((r, i) => { + const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""; + const source = r.path ? ` [${r.path}]` : ""; + return `${i + 1}. ${r.text}${score}${source}`; + }) + .join("\n\n"); + + logger.info( + `[memory:${pluginId}] Found ${results.length} memories for query` + ); + + return `## Recalled Memories\n\n${formatted}`; + } + + return ""; + } catch (error) { + logger.error(`[memory:${pluginId}] Recall failed:`, { error }); + return ""; + } + }, + + async save( + content: string, + metadata?: Record + ): Promise { + try { + if (memory.save) { + await memory.save(content, metadata); + logger.info(`[memory:${pluginId}] Saved ${content.length} chars`); + return; + } + + if (memory.indexChunk) { + const timestamp = new Date().toISOString(); + await memory.indexChunk( + `memory/${timestamp.split("T")[0]}.md`, + content, + metadata + ); + logger.info(`[memory:${pluginId}] Indexed ${content.length} chars`); + } + } catch (error) { + logger.error(`[memory:${pluginId}] Save failed:`, { error }); + } + }, + }; +} + +// ============================================================================ +// Provider Plugin Bridge +// ============================================================================ + +/** + * Extract provider information from loaded plugins. + * Returns model metadata that can be used for model selection. + */ +export function bridgeProviderPlugins(plugins: LoadedPlugin[]): Array<{ + pluginId: string; + models: Array<{ id: string; name: string; api?: string }>; +}> { + const providers: Array<{ + pluginId: string; + models: Array<{ id: string; name: string; api?: string }>; + }> = []; + + for (const plugin of plugins) { + if ( + plugin.registrations.slot !== "provider" || + !plugin.registrations.provider + ) { + continue; + } + + const provider = plugin.registrations.provider; + if (provider.models && provider.models.length > 0) { + providers.push({ + pluginId: plugin.manifest.id, + models: provider.models, + }); + logger.info( + `Provider plugin ${plugin.manifest.id}: ${provider.models.length} models available` + ); + } + } + + return providers; +} + +// ============================================================================ +// Unified Bridge +// ============================================================================ + +/** + * Bridge result containing all adapted plugin components. + */ +export interface PluginBridgeResult { + /** Tool definitions for createAgentSession({ customTools }) */ + tools: ToolDefinition[]; + /** Memory hooks for recall/save lifecycle */ + memory: MemoryHooks | null; + /** Provider model info */ + providers: Array<{ + pluginId: string; + models: Array<{ id: string; name: string; api?: string }>; + }>; +} + +/** + * Bridge all loaded plugins into worker-consumable components. + */ +export function bridgePlugins(plugins: LoadedPlugin[]): PluginBridgeResult { + return { + tools: bridgePluginTools(plugins), + memory: bridgeMemoryPlugin(plugins), + providers: bridgeProviderPlugins(plugins), + }; +} diff --git a/packages/worker/src/openclaw/worker.ts b/packages/worker/src/openclaw/worker.ts index cc7c9d68a..0e118d5ab 100644 --- a/packages/worker/src/openclaw/worker.ts +++ b/packages/worker/src/openclaw/worker.ts @@ -2,7 +2,12 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { createLogger, type ToolsConfig } from "@lobu/core"; +import { + createLogger, + type LoadedPlugin, + type PluginsConfig, + type ToolsConfig, +} from "@lobu/core"; import { getModel } from "@mariozechner/pi-ai"; import { AuthStorage, @@ -19,6 +24,7 @@ import type { } from "../core/types"; import { createOpenClawCustomTools } from "./custom-tools"; import { OpenClawCoreInstructionProvider } from "./instructions"; +import { bridgePlugins, type MemoryHooks } from "./plugin-bridge"; import { OpenClawProgressProcessor } from "./processor"; import { getOpenClawSessionContext } from "./session-context"; import { @@ -166,6 +172,38 @@ Use it when the user references past discussions or you need context.`); platform: this.config.platform, }); + // Load and bridge OpenClaw plugins (tool, memory, provider) + let memoryHooks: MemoryHooks | null = null; + const pluginsConfig = rawOptions.pluginsConfig as PluginsConfig | undefined; + const loadedPlugins: LoadedPlugin[] = pluginsConfig + ? await this.loadPlugins(pluginsConfig) + : []; + + if (loadedPlugins.length > 0) { + const bridged = bridgePlugins(loadedPlugins); + + // Merge plugin tools with custom tools + if (bridged.tools.length > 0) { + customTools.push(...bridged.tools); + logger.info(`Added ${bridged.tools.length} plugin tools`); + } + + // Store memory hooks for lifecycle integration + memoryHooks = bridged.memory; + if (memoryHooks) { + logger.info( + "Memory plugin active -- will recall before prompt and save after" + ); + } + + // Log provider plugins (model selection handled externally) + if (bridged.providers.length > 0) { + logger.info( + `Provider plugins available: ${bridged.providers.map((p) => p.pluginId).join(", ")}` + ); + } + } + logger.info( `Starting OpenClaw session: provider=${provider}, model=${modelId}, tools=${tools.length}, customTools=${customTools.length}` ); @@ -277,8 +315,58 @@ Use it when the user references past discussions or you need context.`); }); }, HEARTBEAT_INTERVAL_MS); - await session.prompt(userPrompt); + // Memory plugin: recall relevant context before prompting + let augmentedPrompt = userPrompt; + if (memoryHooks) { + try { + const recalled = await memoryHooks.recall(userPrompt); + if (recalled) { + augmentedPrompt = `${recalled}\n\n---\n\n${userPrompt}`; + logger.info( + `Prepended ${recalled.length} chars of recalled memory` + ); + } + } catch (error) { + logger.error("Memory recall failed (continuing without):", { error }); + } + } + + await session.prompt(augmentedPrompt); await done; + + // Memory plugin: save conversation context after response + if (memoryHooks) { + try { + const context = session.sessionManager?.buildSessionContext?.(); + const lastMessages = context?.messages?.slice(-2) || []; + const saveContent = lastMessages + .map((m: any) => { + const role = m.role || "unknown"; + const text = + typeof m.content === "string" + ? m.content + : Array.isArray(m.content) + ? m.content + .filter((b: any) => b.type === "text") + .map((b: any) => b.text) + .join("\n") + : ""; + return `${role}: ${text}`; + }) + .join("\n\n"); + + if (saveContent.trim()) { + await memoryHooks.save(saveContent, { + conversationId: this.config.conversationId, + userId: this.config.userId, + timestamp: Date.now(), + }); + } + } catch (error) { + logger.error("Memory save failed:", { error }); + } + } + session.dispose(); return { success: true, @@ -331,6 +419,127 @@ Use it when the user references past discussions or you need context.`); logger.info("Cleanup for OpenClaw session (no-op)"); } + /** + * Load OpenClaw plugins from the configured PluginsConfig. + * Uses dynamic import to load plugin entry points from node_modules or local paths. + */ + private async loadPlugins( + pluginsConfig: PluginsConfig + ): Promise { + const loaded: LoadedPlugin[] = []; + + for (const [pluginId, config] of Object.entries(pluginsConfig.plugins)) { + if (!config.enabled) continue; + + try { + // Try to resolve the plugin module + const modulePath = config.source.startsWith(".") + ? path.resolve(this.getWorkingDirectory(), config.source) + : config.source; + + logger.info(`Loading plugin: ${pluginId} from ${modulePath}`); + + const module = await import(modulePath); + const exported = module.default || module; + + // Create a shim API to capture registrations + const registrations: import("@lobu/core").PluginRegistrations = { + id: pluginId, + tools: [], + channels: [], + memory: null, + provider: null, + services: [], + commands: new Map(), + gatewayMethods: new Map(), + }; + + const shimApi = { + logger: { + info: (...args: unknown[]) => + logger.info(`[plugin:${pluginId}]`, ...args), + warn: (...args: unknown[]) => + logger.warn(`[plugin:${pluginId}]`, ...args), + error: (...args: unknown[]) => + logger.error(`[plugin:${pluginId}]`, ...args), + debug: (...args: unknown[]) => + logger.debug(`[plugin:${pluginId}]`, ...args), + }, + config: config.config || {}, + runtime: { tts: null, stt: null }, + registerTool: (def: any) => { + registrations.tools.push(def); + }, + registerChannel: (opts: any) => { + registrations.channels.push(opts.plugin); + registrations.slot = "channel"; + }, + registerProvider: (def: any) => { + registrations.provider = def; + registrations.slot = "provider"; + }, + registerService: (def: any) => { + registrations.services.push(def); + }, + registerGatewayMethod: () => { + // no-op in worker context + }, + registerCommand: () => { + // no-op in worker context + }, + registerCli: () => { + // no-op in worker context + }, + on: () => { + // no-op in worker context + }, + }; + + if (typeof exported === "function") { + await exported(shimApi); + } else if (typeof exported === "object" && exported !== null) { + if (typeof exported.register === "function") { + await exported.register(shimApi); + } else if (typeof exported.init === "function") { + const result = await exported.init(config.config || {}, { + logger: shimApi.logger, + configDir: process.env.HOME || "/tmp", + workspaceDir: this.getWorkingDirectory(), + rpc: {}, + }); + + // Route init result based on slot type + const slot = exported.slot || exported.kind; + if (slot === "memory") { + registrations.memory = result; + registrations.slot = "memory"; + } else if (slot === "provider") { + registrations.provider = result; + registrations.slot = "provider"; + } else if (slot === "tool" && Array.isArray(result)) { + registrations.tools.push(...result); + registrations.slot = "tool"; + } + } + } + + loaded.push({ + manifest: { id: pluginId, name: pluginId }, + config, + registrations, + }); + + logger.info( + `Plugin ${pluginId} loaded: ${registrations.tools.length} tools, memory=${!!registrations.memory}` + ); + } catch (error) { + logger.error(`Failed to load plugin ${pluginId}:`, { error }); + } + } + + return loaded; + } + private buildPendingInteractionNote( unanswered: Array<{ type: string; question: string }> ): string { From e1a24ebdec885381494b7dd7488ca420b79c6141 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 16:20:15 +0000 Subject: [PATCH 2/2] refactor: strip channel and provider plugin slots, keep tool + memory only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw channel plugins can't support streaming, interactions, or file handling — Lobu's native platform adapters (Slack/Telegram/WhatsApp) are far richer. Provider plugins require deep pi-coding-agent integration that isn't needed yet. Removed: - channel-adapter.ts (entire file) - Channel/provider types (OpenClawChannelDef, OpenClawProviderDef, etc.) - Channel plugin loading from gateway startup - Provider bridge from plugin-bridge.ts - Channel/provider registration from both gateway and worker shim APIs Kept: tool + memory plugin slots with build-time npm installation. https://claude.ai/code/session_01TnnWVhe45TgmLSSbKBzYx3 --- packages/core/src/index.ts | 4 - packages/core/src/plugin-types.ts | 73 +---- packages/gateway/src/cli/gateway.ts | 46 ---- .../gateway/src/plugins/channel-adapter.ts | 255 ------------------ packages/gateway/src/plugins/plugin-loader.ts | 134 +++------ packages/worker/src/openclaw/plugin-bridge.ts | 49 ---- packages/worker/src/openclaw/worker.ts | 42 +-- 7 files changed, 47 insertions(+), 556 deletions(-) delete mode 100644 packages/gateway/src/plugins/channel-adapter.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ed5b9ad0..ec8ed80bb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,12 +67,8 @@ export type { // Plugin types export type { LoadedPlugin, - OpenClawChannelDef, - OpenClawChannelOutbound, OpenClawMemoryDef, OpenClawMemoryResult, - OpenClawProviderDef, - OpenClawServiceDef, OpenClawToolDef, PluginConfig, PluginManifest, diff --git a/packages/core/src/plugin-types.ts b/packages/core/src/plugin-types.ts index 101b4bb40..b82134cd4 100644 --- a/packages/core/src/plugin-types.ts +++ b/packages/core/src/plugin-types.ts @@ -2,11 +2,9 @@ * OpenClaw Plugin System Types * * Defines the interfaces for loading, configuring, and bridging OpenClaw plugins - * into Lobu's architecture. Supports all four plugin slot types: + * into Lobu's architecture. Supports two plugin slot types: * - tool: Agent capabilities (tools available during turns) * - memory: Context recall/save backends (exclusive slot - one active at a time) - * - channel: Messaging platform integrations - * - provider: AI model inference backends */ // ============================================================================ @@ -14,11 +12,11 @@ // ============================================================================ /** Slot types supported by OpenClaw plugins */ -export type PluginSlot = "tool" | "memory" | "channel" | "provider"; +export type PluginSlot = "tool" | "memory"; /** Individual plugin configuration in agent settings */ export interface PluginConfig { - /** npm package name or local path (e.g., "@openclaw/msteams", "./extensions/matrix") */ + /** npm package name or local path (e.g., "@openclaw/tool-websearch", "./extensions/memory-rag") */ source: string; /** Whether this plugin is currently enabled */ enabled: boolean; @@ -45,8 +43,6 @@ export interface PluginManifest { description?: string; kind?: PluginSlot; configSchema?: Record; - channels?: string[]; - providers?: string[]; skills?: string[]; } @@ -69,41 +65,6 @@ export interface OpenClawToolDef { }>; } -/** Channel plugin outbound interface */ -export interface OpenClawChannelOutbound { - deliveryMode: "direct" | "buffered"; - sendText: (params: { - text: string; - [key: string]: unknown; - }) => Promise<{ ok: boolean }>; - sendMedia?: (params: { [key: string]: unknown }) => Promise<{ ok: boolean }>; -} - -/** Channel plugin as registered by OpenClaw plugins */ -export interface OpenClawChannelDef { - id: string; - meta: { - id: string; - label: string; - docsPath?: string; - blurb?: string; - aliases?: string[]; - }; - capabilities: { - chatTypes: Array<"direct" | "group" | "thread" | "channel">; - reactions?: boolean; - threads?: boolean; - media?: boolean; - }; - config: { - listAccountIds: (cfg: unknown) => string[]; - resolveAccount: (cfg: unknown, accountId?: string) => unknown; - }; - outbound: OpenClawChannelOutbound; - startAccount?: (accountId: string) => Promise; - stopAccount?: (accountId: string) => Promise; -} - /** Memory plugin interface */ export interface OpenClawMemoryDef { indexChunk?: ( @@ -126,29 +87,6 @@ export interface OpenClawMemoryResult { metadata?: unknown; } -/** Provider plugin interface */ -export interface OpenClawProviderDef { - id: string; - models?: Array<{ - id: string; - name: string; - api?: string; - contextWindow?: number; - maxTokens?: number; - }>; - stream?: (messages: unknown[], options?: unknown) => AsyncIterable; - validateAuth?: () => Promise; -} - -/** Service registration (background services, HTTP endpoints) */ -export interface OpenClawServiceDef { - type: "http" | "background"; - path?: string; - handler?: (req: unknown, res: unknown) => Promise; - start?: () => Promise; - stop?: () => Promise; -} - /** * Collected registrations from an OpenClaw plugin. * The plugin loader calls register(api) and captures all registrations here. @@ -157,12 +95,7 @@ export interface PluginRegistrations { id: string; slot?: PluginSlot; tools: OpenClawToolDef[]; - channels: OpenClawChannelDef[]; memory: OpenClawMemoryDef | null; - provider: OpenClawProviderDef | null; - services: OpenClawServiceDef[]; - commands: Map unknown>; - gatewayMethods: Map unknown>; } /** diff --git a/packages/gateway/src/cli/gateway.ts b/packages/gateway/src/cli/gateway.ts index 276f713e7..54ca66c20 100644 --- a/packages/gateway/src/cli/gateway.ts +++ b/packages/gateway/src/cli/gateway.ts @@ -855,52 +855,6 @@ export async function startGateway( gateway.registerPlatform(apiPlatform); logger.info("API platform registered"); - // Load and register OpenClaw channel plugins - try { - const { loadPlugins, getPluginsBySlot } = await import( - "../plugins/plugin-loader" - ); - const { createChannelAdapters } = await import( - "../plugins/channel-adapter" - ); - - // Load global plugin config from env or default - const globalPluginsConfigPath = process.env.LOBU_PLUGINS_CONFIG; - if (globalPluginsConfigPath) { - const { readFile } = await import("node:fs/promises"); - const configContent = await readFile(globalPluginsConfigPath, "utf-8"); - const pluginsConfig = JSON.parse(configContent); - - const plugins = await loadPlugins(pluginsConfig, { - baseDir: process.cwd(), - extensionDirs: process.env.LOBU_EXTENSIONS_DIR - ? [process.env.LOBU_EXTENSIONS_DIR] - : [], - }); - - // Register channel plugins as platform adapters - const channelPlugins = getPluginsBySlot(plugins, "channel"); - if (channelPlugins.length > 0) { - const adapters = createChannelAdapters(channelPlugins); - for (const adapter of adapters) { - gateway.registerPlatform(adapter); - logger.info(`OpenClaw channel plugin registered: ${adapter.name}`); - } - } - - logger.info( - `OpenClaw plugins loaded: ${plugins.length} total, ${channelPlugins.length} channel adapters` - ); - } - } catch (error) { - logger.debug( - "OpenClaw plugins: none loaded (this is normal if not configured)", - { - error: error instanceof Error ? error.message : String(error), - } - ); - } - // Start gateway await gateway.start(); logger.info("Gateway started"); diff --git a/packages/gateway/src/plugins/channel-adapter.ts b/packages/gateway/src/plugins/channel-adapter.ts deleted file mode 100644 index 84fdc4b9a..000000000 --- a/packages/gateway/src/plugins/channel-adapter.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * OpenClaw Channel Plugin → Lobu PlatformAdapter - * - * Wraps an OpenClaw channel plugin as a Lobu PlatformAdapter so it can - * be registered with the gateway and participate in message routing. - */ - -import { - createLogger, - type LoadedPlugin, - type OpenClawChannelDef, - type ThreadResponsePayload, -} from "@lobu/core"; -import type { CoreServices, PlatformAdapter } from "../platform"; -import type { ResponseRenderer } from "../platform/response-renderer"; - -const logger = createLogger("channel-adapter"); - -/** - * ResponseRenderer for OpenClaw channel plugins. - * Converts Lobu ThreadResponsePayload into channel outbound calls. - */ -class PluginChannelResponseRenderer implements ResponseRenderer { - private channelDef: OpenClawChannelDef; - private messageBuffers = new Map(); - - constructor(channelDef: OpenClawChannelDef) { - this.channelDef = channelDef; - } - - async handleDelta( - payload: ThreadResponsePayload, - _sessionKey: string - ): Promise { - const key = `${payload.channelId}:${payload.conversationId}`; - - if (payload.isFullReplacement) { - this.messageBuffers.set(key, payload.delta || ""); - } else { - const existing = this.messageBuffers.get(key) || ""; - this.messageBuffers.set(key, existing + (payload.delta || "")); - } - - // Deliver current buffer - const text = this.messageBuffers.get(key) || ""; - try { - await this.channelDef.outbound.sendText({ - text, - channelId: payload.channelId, - conversationId: payload.conversationId, - }); - } catch (error) { - logger.error(`[channel:${this.channelDef.id}] Failed to send delta:`, { - error, - }); - } - - return null; - } - - async handleCompletion( - payload: ThreadResponsePayload, - _sessionKey: string - ): Promise { - const key = `${payload.channelId}:${payload.conversationId}`; - const finalText = this.messageBuffers.get(key); - - if (finalText) { - try { - await this.channelDef.outbound.sendText({ - text: finalText, - channelId: payload.channelId, - conversationId: payload.conversationId, - isFinal: true, - }); - } catch (error) { - logger.error( - `[channel:${this.channelDef.id}] Failed to send completion:`, - { error } - ); - } - } - - this.messageBuffers.delete(key); - } - - async handleError( - payload: ThreadResponsePayload, - _sessionKey: string - ): Promise { - try { - await this.channelDef.outbound.sendText({ - text: `Error: ${payload.error || "Unknown error"}`, - channelId: payload.channelId, - conversationId: payload.conversationId, - }); - } catch (error) { - logger.error(`[channel:${this.channelDef.id}] Failed to send error:`, { - error, - }); - } - - const key = `${payload.channelId}:${payload.conversationId}`; - this.messageBuffers.delete(key); - } - - async handleStatusUpdate(_payload: ThreadResponsePayload): Promise { - // Status updates (heartbeats) -- plugins can show typing indicators here - logger.debug(`[channel:${this.channelDef.id}] Status update (no-op)`); - } - - async handleEphemeral(payload: ThreadResponsePayload): Promise { - try { - await this.channelDef.outbound.sendText({ - text: payload.content || "", - channelId: payload.channelId, - conversationId: payload.conversationId, - ephemeral: true, - }); - } catch (error) { - logger.error( - `[channel:${this.channelDef.id}] Failed to send ephemeral:`, - { error } - ); - } - } -} - -/** - * Wraps an OpenClaw channel plugin as a Lobu PlatformAdapter. - */ -export class PluginChannelAdapter implements PlatformAdapter { - readonly name: string; - private channelDef: OpenClawChannelDef; - private plugin: LoadedPlugin; - private responseRenderer: PluginChannelResponseRenderer; - private running = false; - - constructor(plugin: LoadedPlugin, channelDef: OpenClawChannelDef) { - this.plugin = plugin; - this.channelDef = channelDef; - this.name = channelDef.id; - this.responseRenderer = new PluginChannelResponseRenderer(channelDef); - } - - async initialize(_services: CoreServices): Promise { - logger.info(`[channel:${this.name}] Initialized with core services`); - } - - async start(): Promise { - if (this.channelDef.startAccount) { - const accountIds = this.channelDef.config.listAccountIds( - this.plugin.config.config || {} - ); - for (const accountId of accountIds) { - try { - await this.channelDef.startAccount(accountId); - logger.info(`[channel:${this.name}] Started account: ${accountId}`); - } catch (error) { - logger.error( - `[channel:${this.name}] Failed to start account ${accountId}:`, - { error } - ); - } - } - } - this.running = true; - logger.info(`[channel:${this.name}] Platform started`); - } - - async stop(): Promise { - if (this.channelDef.stopAccount) { - const accountIds = this.channelDef.config.listAccountIds( - this.plugin.config.config || {} - ); - for (const accountId of accountIds) { - try { - await this.channelDef.stopAccount(accountId); - } catch (error) { - logger.error( - `[channel:${this.name}] Failed to stop account ${accountId}:`, - { error } - ); - } - } - } - this.running = false; - logger.info(`[channel:${this.name}] Platform stopped`); - } - - isHealthy(): boolean { - return this.running; - } - - getResponseRenderer(): ResponseRenderer | undefined { - return this.responseRenderer; - } - - buildDeploymentMetadata( - conversationId: string, - channelId: string, - _platformMetadata: Record - ): Record { - return { - platform: this.name, - channelId, - conversationId, - }; - } - - getDisplayInfo() { - return { - name: this.channelDef.meta.label, - icon: "🔌", - }; - } - - async sendMessage( - _token: string, - message: string, - options: { - agentId: string; - channelId: string; - conversationId?: string; - teamId: string; - } - ): Promise<{ messageId: string }> { - await this.channelDef.outbound.sendText({ - text: message, - channelId: options.channelId, - conversationId: options.conversationId, - }); - return { messageId: `plugin-${Date.now()}` }; - } -} - -/** - * Create PlatformAdapters for all channel plugins. - */ -export function createChannelAdapters( - plugins: LoadedPlugin[] -): PluginChannelAdapter[] { - const adapters: PluginChannelAdapter[] = []; - - for (const plugin of plugins) { - for (const channelDef of plugin.registrations.channels) { - adapters.push(new PluginChannelAdapter(plugin, channelDef)); - logger.info( - `Created channel adapter: ${channelDef.id} (${channelDef.meta.label})` - ); - } - } - - return adapters; -} diff --git a/packages/gateway/src/plugins/plugin-loader.ts b/packages/gateway/src/plugins/plugin-loader.ts index dd5ec677a..486ab4fed 100644 --- a/packages/gateway/src/plugins/plugin-loader.ts +++ b/packages/gateway/src/plugins/plugin-loader.ts @@ -2,9 +2,8 @@ * OpenClaw Plugin Loader * * Discovers, validates, and loads OpenClaw plugins. Provides a shim - * OpenClawPluginApi that captures all registrations (tools, channels, - * memory, providers, services) so Lobu can route them to the appropriate - * subsystems. + * OpenClawPluginApi that captures tool and memory registrations so Lobu + * can route them to the worker's agent session. * * Discovery sources: * - node_modules/@openclaw/* @@ -18,10 +17,7 @@ import * as path from "node:path"; import { createLogger, type LoadedPlugin, - type OpenClawChannelDef, type OpenClawMemoryDef, - type OpenClawProviderDef, - type OpenClawServiceDef, type OpenClawToolDef, type PluginConfig, type PluginManifest, @@ -36,7 +32,7 @@ const logger = createLogger("plugin-loader"); // ============================================================================ /** - * Creates a shim OpenClawPluginApi that captures all registrations + * Creates a shim OpenClawPluginApi that captures tool and memory registrations * from a plugin's register() or init() call. */ function createPluginApiShim( @@ -46,12 +42,7 @@ function createPluginApiShim( const registrations: PluginRegistrations = { id: pluginId, tools: [], - channels: [], memory: null, - provider: null, - services: [], - commands: new Map(), - gatewayMethods: new Map(), }; const api = { @@ -69,7 +60,6 @@ function createPluginApiShim( config: pluginConfig, runtime: { - // Stub runtime utilities -- plugins can check capabilities tts: null, stt: null, }, @@ -79,48 +69,39 @@ function createPluginApiShim( registrations.tools.push(definition); }, - registerChannel(opts: { plugin: OpenClawChannelDef }) { - logger.info(`Plugin ${pluginId} registered channel: ${opts.plugin.id}`); - registrations.channels.push(opts.plugin); - registrations.slot = "channel"; + // Unsupported slot registrations — log and ignore + registerChannel() { + logger.debug( + `Plugin ${pluginId} tried to register channel (not supported in Lobu)` + ); }, - registerProvider(config: OpenClawProviderDef) { - logger.info(`Plugin ${pluginId} registered provider: ${config.id}`); - registrations.provider = config; - registrations.slot = "provider"; + registerProvider() { + logger.debug( + `Plugin ${pluginId} tried to register provider (not supported in Lobu)` + ); }, - registerService(config: OpenClawServiceDef) { - logger.info(`Plugin ${pluginId} registered service: ${config.type}`); - registrations.services.push(config); + registerService() { + logger.debug( + `Plugin ${pluginId} tried to register service (not supported in Lobu)` + ); }, - registerGatewayMethod( - name: string, - handler: (...args: unknown[]) => unknown - ) { - logger.info(`Plugin ${pluginId} registered gateway method: ${name}`); - registrations.gatewayMethods.set(name, handler); + registerGatewayMethod() { + // no-op }, - registerCommand(options: { - name: string; - handler: (...args: unknown[]) => unknown; - }) { - logger.info(`Plugin ${pluginId} registered command: ${options.name}`); - registrations.commands.set(options.name, options.handler); + registerCommand() { + // no-op }, registerCli() { - logger.debug( - `Plugin ${pluginId} registered CLI (ignored in Lobu context)` - ); + // no-op }, - // Event subscription (no-op in shim -- lifecycle hooks handled separately) - on(event: string) { - logger.debug(`Plugin ${pluginId} subscribed to event: ${event}`); + on() { + // no-op }, }; @@ -155,7 +136,7 @@ async function readPluginManifest( /** * Discover OpenClaw plugins from a node_modules directory. - * Scans @openclaw/_ and @_/openclaw-_ packages (community namespace). + * Scans @openclaw/* and @* /openclaw-* packages (community namespace). */ async function discoverFromNodeModules( baseDir: string @@ -178,7 +159,7 @@ async function discoverFromNodeModules( if (plugin) discovered.push(plugin); } } catch { - // @openclaw directory doesn't exist -- that's fine + // @openclaw directory doesn't exist } // Scan @*/openclaw-* packages (community namespace) @@ -305,62 +286,15 @@ async function loadPlugin( logger: api.logger, configDir: process.env.HOME || "/tmp", workspaceDir: process.cwd(), - rpc: {}, // Stub -- will be wired at runtime + rpc: {}, }); // Capture slot-specific registrations from init result - if (exported.slot === "memory" || exported.kind === "memory") { + const slot = exported.slot || exported.kind; + if (slot === "memory") { registrations.memory = result as OpenClawMemoryDef; registrations.slot = "memory"; - } else if ( - exported.slot === "provider" || - exported.kind === "provider" - ) { - registrations.provider = result as OpenClawProviderDef; - registrations.slot = "provider"; - } else if ( - exported.slot === "channel" || - exported.kind === "channel" - ) { - // Channel plugins return { start, stop, send } from init - if (result && typeof result === "object") { - const channelResult = result as { - start?: () => Promise; - stop?: () => Promise; - send?: (envelope: unknown) => Promise; - }; - registrations.channels.push({ - id: exported.id || pluginId, - meta: { - id: exported.id || pluginId, - label: exported.metadata?.name || pluginId, - docsPath: "", - }, - capabilities: { chatTypes: ["direct", "group"] }, - config: { - listAccountIds: () => ["default"], - resolveAccount: () => ({}), - }, - outbound: { - deliveryMode: "direct", - sendText: async (params) => { - if (channelResult.send) { - await channelResult.send(params); - } - return { ok: true }; - }, - }, - startAccount: channelResult.start - ? async () => channelResult.start!() - : undefined, - stopAccount: channelResult.stop - ? async () => channelResult.stop!() - : undefined, - }); - registrations.slot = "channel"; - } - } else if (exported.slot === "tool") { - // Tool plugins return tool definitions from init + } else if (slot === "tool") { if (Array.isArray(result)) { for (const tool of result) { registrations.tools.push(tool as OpenClawToolDef); @@ -390,7 +324,7 @@ async function loadPlugin( }; logger.info( - `Plugin ${pluginId} loaded: ${registrations.tools.length} tools, ${registrations.channels.length} channels, memory=${!!registrations.memory}, provider=${!!registrations.provider}` + `Plugin ${pluginId} loaded: ${registrations.tools.length} tools, memory=${!!registrations.memory}` ); return { @@ -413,7 +347,6 @@ export interface PluginLoaderOptions { /** * Discover all available OpenClaw plugins (without loading them). - * Returns plugin IDs and metadata for configuration UI. */ export async function discoverPlugins( options?: PluginLoaderOptions @@ -421,11 +354,9 @@ export async function discoverPlugins( const baseDir = options?.baseDir || process.cwd(); const discovered: DiscoveredPlugin[] = []; - // Discover from node_modules const fromNpm = await discoverFromNodeModules(baseDir); discovered.push(...fromNpm); - // Discover from extension directories for (const dir of options?.extensionDirs || []) { const fromDir = await discoverFromExtensions(dir); discovered.push(...fromDir); @@ -437,7 +368,6 @@ export async function discoverPlugins( /** * Load all enabled plugins from a PluginsConfig. - * Returns loaded plugins organized by slot type. */ export async function loadPlugins( pluginsConfig: PluginsConfig, @@ -478,7 +408,7 @@ export async function loadPlugins( const plugin = await loadPlugin(found, config); if (plugin) { - // Check exclusive slot constraints + // Check exclusive slot constraints (memory is exclusive — one at a time) if (pluginsConfig.slots && plugin.registrations.slot) { const slotAssignment = pluginsConfig.slots[plugin.registrations.slot]; if (slotAssignment && slotAssignment !== pluginId) { @@ -521,5 +451,5 @@ export function getActiveMemoryPlugin( plugins: LoadedPlugin[] ): LoadedPlugin | undefined { const memoryPlugins = getPluginsBySlot(plugins, "memory"); - return memoryPlugins[0]; // First one wins (slot assignment handled during loading) + return memoryPlugins[0]; } diff --git a/packages/worker/src/openclaw/plugin-bridge.ts b/packages/worker/src/openclaw/plugin-bridge.ts index 5dea4a542..11eb78691 100644 --- a/packages/worker/src/openclaw/plugin-bridge.ts +++ b/packages/worker/src/openclaw/plugin-bridge.ts @@ -4,9 +4,6 @@ * Bridges OpenClaw plugin registrations into the pi-coding-agent runtime: * - Tool plugins → ToolDefinition objects injected into createAgentSession * - Memory plugins → before_agent_start (recall) and agent_end (save) hooks - * - Provider plugins → model registration (future) - * - * Channel plugins are handled gateway-side (see gateway/src/plugins/channel-adapter.ts). */ import { @@ -207,46 +204,6 @@ export function bridgeMemoryPlugin( }; } -// ============================================================================ -// Provider Plugin Bridge -// ============================================================================ - -/** - * Extract provider information from loaded plugins. - * Returns model metadata that can be used for model selection. - */ -export function bridgeProviderPlugins(plugins: LoadedPlugin[]): Array<{ - pluginId: string; - models: Array<{ id: string; name: string; api?: string }>; -}> { - const providers: Array<{ - pluginId: string; - models: Array<{ id: string; name: string; api?: string }>; - }> = []; - - for (const plugin of plugins) { - if ( - plugin.registrations.slot !== "provider" || - !plugin.registrations.provider - ) { - continue; - } - - const provider = plugin.registrations.provider; - if (provider.models && provider.models.length > 0) { - providers.push({ - pluginId: plugin.manifest.id, - models: provider.models, - }); - logger.info( - `Provider plugin ${plugin.manifest.id}: ${provider.models.length} models available` - ); - } - } - - return providers; -} - // ============================================================================ // Unified Bridge // ============================================================================ @@ -259,11 +216,6 @@ export interface PluginBridgeResult { tools: ToolDefinition[]; /** Memory hooks for recall/save lifecycle */ memory: MemoryHooks | null; - /** Provider model info */ - providers: Array<{ - pluginId: string; - models: Array<{ id: string; name: string; api?: string }>; - }>; } /** @@ -273,6 +225,5 @@ export function bridgePlugins(plugins: LoadedPlugin[]): PluginBridgeResult { return { tools: bridgePluginTools(plugins), memory: bridgeMemoryPlugin(plugins), - providers: bridgeProviderPlugins(plugins), }; } diff --git a/packages/worker/src/openclaw/worker.ts b/packages/worker/src/openclaw/worker.ts index 0e118d5ab..f5babe80c 100644 --- a/packages/worker/src/openclaw/worker.ts +++ b/packages/worker/src/openclaw/worker.ts @@ -172,7 +172,7 @@ Use it when the user references past discussions or you need context.`); platform: this.config.platform, }); - // Load and bridge OpenClaw plugins (tool, memory, provider) + // Load and bridge OpenClaw plugins (tool + memory) let memoryHooks: MemoryHooks | null = null; const pluginsConfig = rawOptions.pluginsConfig as PluginsConfig | undefined; const loadedPlugins: LoadedPlugin[] = pluginsConfig @@ -182,26 +182,17 @@ Use it when the user references past discussions or you need context.`); if (loadedPlugins.length > 0) { const bridged = bridgePlugins(loadedPlugins); - // Merge plugin tools with custom tools if (bridged.tools.length > 0) { customTools.push(...bridged.tools); logger.info(`Added ${bridged.tools.length} plugin tools`); } - // Store memory hooks for lifecycle integration memoryHooks = bridged.memory; if (memoryHooks) { logger.info( "Memory plugin active -- will recall before prompt and save after" ); } - - // Log provider plugins (model selection handled externally) - if (bridged.providers.length > 0) { - logger.info( - `Provider plugins available: ${bridged.providers.map((p) => p.pluginId).join(", ")}` - ); - } } logger.info( @@ -446,12 +437,7 @@ Use it when the user references past discussions or you need context.`); const registrations: import("@lobu/core").PluginRegistrations = { id: pluginId, tools: [], - channels: [], memory: null, - provider: null, - services: [], - commands: new Map(), - gatewayMethods: new Map(), }; const shimApi = { @@ -470,28 +456,27 @@ Use it when the user references past discussions or you need context.`); registerTool: (def: any) => { registrations.tools.push(def); }, - registerChannel: (opts: any) => { - registrations.channels.push(opts.plugin); - registrations.slot = "channel"; + // Unsupported registrations — no-op in worker + registerChannel: () => { + // no-op }, - registerProvider: (def: any) => { - registrations.provider = def; - registrations.slot = "provider"; + registerProvider: () => { + // no-op }, - registerService: (def: any) => { - registrations.services.push(def); + registerService: () => { + // no-op }, registerGatewayMethod: () => { - // no-op in worker context + // no-op }, registerCommand: () => { - // no-op in worker context + // no-op }, registerCli: () => { - // no-op in worker context + // no-op }, on: () => { - // no-op in worker context + // no-op }, }; @@ -513,9 +498,6 @@ Use it when the user references past discussions or you need context.`); if (slot === "memory") { registrations.memory = result; registrations.slot = "memory"; - } else if (slot === "provider") { - registrations.provider = result; - registrations.slot = "provider"; } else if (slot === "tool" && Array.isArray(result)) { registrations.tools.push(...result); registrations.slot = "tool";