mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
MAESTRO: Fix OpenClaw SDK API mismatch — use real PluginApi interface
E2E testing against the official OpenClaw Docker image revealed the plugin was built against a custom interface that didn't match the real SDK: - api.log() → api.logger.info/warn/error() (PluginLogger interface) - api.getConfig() → api.pluginConfig (direct property) - command handler (args[], ctx) → (ctx) with ctx.args string - service stop optional, service context typed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, any> = {}) {
|
||||
function createMockApi(pluginConfigOverride: Record<string, any> = {}) {
|
||||
const logs: string[] = [];
|
||||
const sentMessages: Array<{ to: string; text: string; channel: string }> = [];
|
||||
|
||||
@@ -11,9 +11,17 @@ function createMockApi(configOverride: Record<string, any> = {}) {
|
||||
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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
workspaceDir?: string;
|
||||
stateDir: string;
|
||||
logger: PluginLogger;
|
||||
}
|
||||
|
||||
interface PluginCommandContext {
|
||||
senderId?: string;
|
||||
channel: string;
|
||||
isAuthorizedSender: boolean;
|
||||
args?: string;
|
||||
commandBody: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type PluginCommandResult = string | { text: string } | { text: string; format?: string };
|
||||
|
||||
interface OpenClawPluginApi {
|
||||
getConfig: () => Record<string, any>;
|
||||
log: (message: string) => void;
|
||||
id: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
source: string;
|
||||
config: Record<string, unknown>;
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
logger: PluginLogger;
|
||||
registerService: (service: {
|
||||
id: string;
|
||||
start: (ctx: any) => Promise<void>;
|
||||
stop: (ctx: any) => Promise<void>;
|
||||
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
||||
stop?: (ctx: PluginServiceContext) => void | Promise<void>;
|
||||
}) => void;
|
||||
registerCommand: (command: {
|
||||
name: string;
|
||||
description: string;
|
||||
handler: (args: string[], ctx: any) => Promise<string>;
|
||||
acceptsArgs?: boolean;
|
||||
requireAuth?: boolean;
|
||||
handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;
|
||||
}) => void;
|
||||
runtime: {
|
||||
channel: Record<string, Record<string, (to: string, text: string) => Promise<any>>>;
|
||||
channel: Record<string, Record<string, (...args: any[]) => Promise<any>>>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,20 +97,20 @@ function sendToChannel(
|
||||
): Promise<void> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user