diff --git a/openclaw/src/index.test.ts b/openclaw/src/index.test.ts index e2f63a00..5635c4dd 100644 --- a/openclaw/src/index.test.ts +++ b/openclaw/src/index.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"; import claudeMemPlugin from "./index.js"; -function createMockApi(configOverride: Record = {}) { +function createMockApi(pluginConfigOverride: Record = {}) { const logs: string[] = []; const sentMessages: Array<{ to: string; text: string; channel: string }> = []; @@ -11,9 +11,17 @@ function createMockApi(configOverride: Record = {}) { let registeredCommand: any = null; const api = { - getConfig: () => configOverride, - log: (message: string) => { - logs.push(message); + id: "claude-mem", + name: "Claude-Mem (Persistent Memory)", + version: "1.0.0", + source: "/test/extensions/claude-mem/dist/index.js", + config: {}, + pluginConfig: pluginConfigOverride, + logger: { + info: (message: string) => { logs.push(message); }, + warn: (message: string) => { logs.push(message); }, + error: (message: string) => { logs.push(message); }, + debug: (message: string) => { logs.push(message); }, }, registerService: (service: any) => { registeredService = service; @@ -133,7 +141,7 @@ describe("claudeMemPlugin", () => { const { api, getCommand } = createMockApi({}); claudeMemPlugin(api); - const result = await getCommand().handler([], {}); + const result = await getCommand().handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); assert.ok(result.includes("not configured")); }); @@ -143,7 +151,7 @@ describe("claudeMemPlugin", () => { }); claudeMemPlugin(api); - const result = await getCommand().handler([], {}); + const result = await getCommand().handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); assert.ok(result.includes("Enabled: yes")); assert.ok(result.includes("Channel: telegram")); assert.ok(result.includes("Target: 123")); @@ -156,7 +164,7 @@ describe("claudeMemPlugin", () => { }); claudeMemPlugin(api); - const result = await getCommand().handler(["on"], {}); + const result = await getCommand().handler({ args: "on", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed on", config: {} }); assert.ok(result.includes("enable requested")); assert.ok(logs.some((l) => l.includes("enable requested"))); }); @@ -167,7 +175,7 @@ describe("claudeMemPlugin", () => { }); claudeMemPlugin(api); - const result = await getCommand().handler(["off"], {}); + const result = await getCommand().handler({ args: "off", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed off", config: {} }); assert.ok(result.includes("disable requested")); assert.ok(logs.some((l) => l.includes("disable requested"))); }); @@ -178,7 +186,7 @@ describe("claudeMemPlugin", () => { }); claudeMemPlugin(api); - const result = await getCommand().handler([], {}); + const result = await getCommand().handler({ args: "", channel: "slack", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); assert.ok(result.includes("Connection: disconnected")); }); }); diff --git a/openclaw/src/index.ts b/openclaw/src/index.ts index 65e0e03a..e13860ed 100644 --- a/openclaw/src/index.ts +++ b/openclaw/src/index.ts @@ -1,18 +1,54 @@ +// Minimal type declarations for the OpenClaw Plugin SDK. +// These match the real OpenClawPluginApi provided by the gateway at runtime. +// See: https://docs.openclaw.ai/plugin + +interface PluginLogger { + debug?: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; +} + +interface PluginServiceContext { + config: Record; + workspaceDir?: string; + stateDir: string; + logger: PluginLogger; +} + +interface PluginCommandContext { + senderId?: string; + channel: string; + isAuthorizedSender: boolean; + args?: string; + commandBody: string; + config: Record; +} + +type PluginCommandResult = string | { text: string } | { text: string; format?: string }; + interface OpenClawPluginApi { - getConfig: () => Record; - log: (message: string) => void; + id: string; + name: string; + version?: string; + source: string; + config: Record; + pluginConfig?: Record; + logger: PluginLogger; registerService: (service: { id: string; - start: (ctx: any) => Promise; - stop: (ctx: any) => Promise; + start: (ctx: PluginServiceContext) => void | Promise; + stop?: (ctx: PluginServiceContext) => void | Promise; }) => void; registerCommand: (command: { name: string; description: string; - handler: (args: string[], ctx: any) => Promise; + acceptsArgs?: boolean; + requireAuth?: boolean; + handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise; }) => void; runtime: { - channel: Record Promise>>; + channel: Record Promise>>; }; } @@ -61,20 +97,20 @@ function sendToChannel( ): Promise { const channelApi = api.runtime.channel[channel]; if (!channelApi) { - api.log(`[claude-mem] Unknown channel type: ${channel}`); + api.logger.warn(`[claude-mem] Unknown channel type: ${channel}`); return Promise.resolve(); } const sendFunctionName = `sendMessage${channel.charAt(0).toUpperCase()}${channel.slice(1)}`; const senderFunction = channelApi[sendFunctionName]; if (!senderFunction) { - api.log(`[claude-mem] Channel "${channel}" has no ${sendFunctionName} function`); + api.logger.warn(`[claude-mem] Channel "${channel}" has no ${sendFunctionName} function`); return Promise.resolve(); } return senderFunction(to, text).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); - api.log(`[claude-mem] Failed to send to ${channel}: ${message}`); + api.logger.error(`[claude-mem] Failed to send to ${channel}: ${message}`); }); } @@ -92,7 +128,7 @@ async function connectToSSEStream( while (!abortController.signal.aborted) { try { setConnectionState("reconnecting"); - api.log(`[claude-mem] Connecting to SSE stream at http://localhost:${port}/stream`); + api.logger.info(`[claude-mem] Connecting to SSE stream at http://localhost:${port}/stream`); const response = await fetch(`http://localhost:${port}/stream`, { signal: abortController.signal, @@ -109,7 +145,7 @@ async function connectToSSEStream( setConnectionState("connected"); backoffMs = 1000; - api.log("[claude-mem] Connected to SSE stream"); + api.logger.info("[claude-mem] Connected to SSE stream"); const reader = response.body.getReader(); const decoder = new TextDecoder(); @@ -122,7 +158,7 @@ async function connectToSSEStream( buffer += decoder.decode(value, { stream: true }); if (buffer.length > MAX_SSE_BUFFER_SIZE) { - api.log("[claude-mem] SSE buffer overflow, clearing buffer"); + api.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"); buffer = ""; } @@ -147,7 +183,7 @@ async function connectToSSEStream( } } catch (parseError: unknown) { const errorMessage = parseError instanceof Error ? parseError.message : String(parseError); - api.log(`[claude-mem] Failed to parse SSE frame: ${errorMessage}`); + api.logger.warn(`[claude-mem] Failed to parse SSE frame: ${errorMessage}`); } } } @@ -157,7 +193,7 @@ async function connectToSSEStream( } setConnectionState("reconnecting"); const errorMessage = error instanceof Error ? error.message : String(error); - api.log(`[claude-mem] SSE stream error: ${errorMessage}. Reconnecting in ${backoffMs / 1000}s`); + api.logger.warn(`[claude-mem] SSE stream error: ${errorMessage}. Reconnecting in ${backoffMs / 1000}s`); } if (abortController.signal.aborted) break; @@ -186,23 +222,23 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void { } } - const config = api.getConfig(); + const config = api.pluginConfig || {}; const workerPort = (config.workerPort as number) || 37777; const feedConfig = config.observationFeed as | { enabled?: boolean; channel?: string; to?: string } | undefined; if (!feedConfig?.enabled) { - api.log("[claude-mem] Observation feed disabled"); + api.logger.info("[claude-mem] Observation feed disabled"); return; } if (!feedConfig.channel || !feedConfig.to) { - api.log("[claude-mem] Observation feed misconfigured — channel or target missing"); + api.logger.warn("[claude-mem] Observation feed misconfigured — channel or target missing"); return; } - api.log(`[claude-mem] Observation feed starting — channel: ${feedConfig.channel}, target: ${feedConfig.to}`); + api.logger.info(`[claude-mem] Observation feed starting — channel: ${feedConfig.channel}, target: ${feedConfig.to}`); sseAbortController = new AbortController(); connectionPromise = connectToSSEStream( @@ -224,15 +260,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void { connectionPromise = null; } connectionState = "disconnected"; - api.log("[claude-mem] Observation feed stopped — SSE connection closed"); + api.logger.info("[claude-mem] Observation feed stopped — SSE connection closed"); }, }); api.registerCommand({ name: "claude-mem-feed", description: "Show or toggle Claude-Mem observation feed status", - handler: async (args, _ctx) => { - const config = api.getConfig(); + acceptsArgs: true, + handler: async (ctx) => { + const config = api.pluginConfig || {}; const feedConfig = config.observationFeed as | { enabled?: boolean; channel?: string; to?: string } | undefined; @@ -241,13 +278,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void { return "Observation feed not configured. Add observationFeed to your plugin config."; } - if (args[0] === "on") { - api.log("[claude-mem] Feed enable requested via command"); + const arg = ctx.args?.trim(); + + if (arg === "on") { + api.logger.info("[claude-mem] Feed enable requested via command"); return "Feed enable requested. Update observationFeed.enabled in your plugin config to persist."; } - if (args[0] === "off") { - api.log("[claude-mem] Feed disable requested via command"); + if (arg === "off") { + api.logger.info("[claude-mem] Feed disable requested via command"); return "Feed disable requested. Update observationFeed.enabled in your plugin config to persist."; } @@ -261,5 +300,5 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void { }, }); - api.log("[claude-mem] OpenClaw plugin loaded — v1.0.0"); + api.logger.info("[claude-mem] OpenClaw plugin loaded — v1.0.0"); } diff --git a/openclaw/test-sse-consumer.js b/openclaw/test-sse-consumer.js index b69a978d..5af5f487 100644 --- a/openclaw/test-sse-consumer.js +++ b/openclaw/test-sse-consumer.js @@ -12,9 +12,17 @@ let registeredCommand = null; const logs = []; const mockApi = { - getConfig: () => ({}), - log: (message) => { - logs.push(message); + id: "claude-mem", + name: "Claude-Mem (Persistent Memory)", + version: "1.0.0", + source: "/test/extensions/claude-mem/dist/index.js", + config: {}, + pluginConfig: {}, + logger: { + info: (message) => { logs.push(message); }, + warn: (message) => { logs.push(message); }, + error: (message) => { logs.push(message); }, + debug: (message) => { logs.push(message); }, }, registerService: (service) => { registeredService = service;