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 { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
import claudeMemPlugin from "./index.js";
|
import claudeMemPlugin from "./index.js";
|
||||||
|
|
||||||
function createMockApi(configOverride: Record<string, any> = {}) {
|
function createMockApi(pluginConfigOverride: Record<string, any> = {}) {
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
const sentMessages: Array<{ to: string; text: string; channel: string }> = [];
|
const sentMessages: Array<{ to: string; text: string; channel: string }> = [];
|
||||||
|
|
||||||
@@ -11,9 +11,17 @@ function createMockApi(configOverride: Record<string, any> = {}) {
|
|||||||
let registeredCommand: any = null;
|
let registeredCommand: any = null;
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
getConfig: () => configOverride,
|
id: "claude-mem",
|
||||||
log: (message: string) => {
|
name: "Claude-Mem (Persistent Memory)",
|
||||||
logs.push(message);
|
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) => {
|
registerService: (service: any) => {
|
||||||
registeredService = service;
|
registeredService = service;
|
||||||
@@ -133,7 +141,7 @@ describe("claudeMemPlugin", () => {
|
|||||||
const { api, getCommand } = createMockApi({});
|
const { api, getCommand } = createMockApi({});
|
||||||
claudeMemPlugin(api);
|
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"));
|
assert.ok(result.includes("not configured"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +151,7 @@ describe("claudeMemPlugin", () => {
|
|||||||
});
|
});
|
||||||
claudeMemPlugin(api);
|
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("Enabled: yes"));
|
||||||
assert.ok(result.includes("Channel: telegram"));
|
assert.ok(result.includes("Channel: telegram"));
|
||||||
assert.ok(result.includes("Target: 123"));
|
assert.ok(result.includes("Target: 123"));
|
||||||
@@ -156,7 +164,7 @@ describe("claudeMemPlugin", () => {
|
|||||||
});
|
});
|
||||||
claudeMemPlugin(api);
|
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(result.includes("enable requested"));
|
||||||
assert.ok(logs.some((l) => l.includes("enable requested")));
|
assert.ok(logs.some((l) => l.includes("enable requested")));
|
||||||
});
|
});
|
||||||
@@ -167,7 +175,7 @@ describe("claudeMemPlugin", () => {
|
|||||||
});
|
});
|
||||||
claudeMemPlugin(api);
|
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(result.includes("disable requested"));
|
||||||
assert.ok(logs.some((l) => l.includes("disable requested")));
|
assert.ok(logs.some((l) => l.includes("disable requested")));
|
||||||
});
|
});
|
||||||
@@ -178,7 +186,7 @@ describe("claudeMemPlugin", () => {
|
|||||||
});
|
});
|
||||||
claudeMemPlugin(api);
|
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"));
|
assert.ok(result.includes("Connection: disconnected"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+65
-26
@@ -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 {
|
interface OpenClawPluginApi {
|
||||||
getConfig: () => Record<string, any>;
|
id: string;
|
||||||
log: (message: string) => void;
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
source: string;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
pluginConfig?: Record<string, unknown>;
|
||||||
|
logger: PluginLogger;
|
||||||
registerService: (service: {
|
registerService: (service: {
|
||||||
id: string;
|
id: string;
|
||||||
start: (ctx: any) => Promise<void>;
|
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
||||||
stop: (ctx: any) => Promise<void>;
|
stop?: (ctx: PluginServiceContext) => void | Promise<void>;
|
||||||
}) => void;
|
}) => void;
|
||||||
registerCommand: (command: {
|
registerCommand: (command: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
handler: (args: string[], ctx: any) => Promise<string>;
|
acceptsArgs?: boolean;
|
||||||
|
requireAuth?: boolean;
|
||||||
|
handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;
|
||||||
}) => void;
|
}) => void;
|
||||||
runtime: {
|
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> {
|
): Promise<void> {
|
||||||
const channelApi = api.runtime.channel[channel];
|
const channelApi = api.runtime.channel[channel];
|
||||||
if (!channelApi) {
|
if (!channelApi) {
|
||||||
api.log(`[claude-mem] Unknown channel type: ${channel}`);
|
api.logger.warn(`[claude-mem] Unknown channel type: ${channel}`);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendFunctionName = `sendMessage${channel.charAt(0).toUpperCase()}${channel.slice(1)}`;
|
const sendFunctionName = `sendMessage${channel.charAt(0).toUpperCase()}${channel.slice(1)}`;
|
||||||
const senderFunction = channelApi[sendFunctionName];
|
const senderFunction = channelApi[sendFunctionName];
|
||||||
if (!senderFunction) {
|
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 Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return senderFunction(to, text).catch((error: unknown) => {
|
return senderFunction(to, text).catch((error: unknown) => {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
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) {
|
while (!abortController.signal.aborted) {
|
||||||
try {
|
try {
|
||||||
setConnectionState("reconnecting");
|
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`, {
|
const response = await fetch(`http://localhost:${port}/stream`, {
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
@@ -109,7 +145,7 @@ async function connectToSSEStream(
|
|||||||
|
|
||||||
setConnectionState("connected");
|
setConnectionState("connected");
|
||||||
backoffMs = 1000;
|
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 reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
@@ -122,7 +158,7 @@ async function connectToSSEStream(
|
|||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
if (buffer.length > MAX_SSE_BUFFER_SIZE) {
|
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 = "";
|
buffer = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +183,7 @@ async function connectToSSEStream(
|
|||||||
}
|
}
|
||||||
} catch (parseError: unknown) {
|
} catch (parseError: unknown) {
|
||||||
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
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");
|
setConnectionState("reconnecting");
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
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;
|
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 workerPort = (config.workerPort as number) || 37777;
|
||||||
const feedConfig = config.observationFeed as
|
const feedConfig = config.observationFeed as
|
||||||
| { enabled?: boolean; channel?: string; to?: string }
|
| { enabled?: boolean; channel?: string; to?: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (!feedConfig?.enabled) {
|
if (!feedConfig?.enabled) {
|
||||||
api.log("[claude-mem] Observation feed disabled");
|
api.logger.info("[claude-mem] Observation feed disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!feedConfig.channel || !feedConfig.to) {
|
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;
|
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();
|
sseAbortController = new AbortController();
|
||||||
connectionPromise = connectToSSEStream(
|
connectionPromise = connectToSSEStream(
|
||||||
@@ -224,15 +260,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
connectionPromise = null;
|
connectionPromise = null;
|
||||||
}
|
}
|
||||||
connectionState = "disconnected";
|
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({
|
api.registerCommand({
|
||||||
name: "claude-mem-feed",
|
name: "claude-mem-feed",
|
||||||
description: "Show or toggle Claude-Mem observation feed status",
|
description: "Show or toggle Claude-Mem observation feed status",
|
||||||
handler: async (args, _ctx) => {
|
acceptsArgs: true,
|
||||||
const config = api.getConfig();
|
handler: async (ctx) => {
|
||||||
|
const config = api.pluginConfig || {};
|
||||||
const feedConfig = config.observationFeed as
|
const feedConfig = config.observationFeed as
|
||||||
| { enabled?: boolean; channel?: string; to?: string }
|
| { enabled?: boolean; channel?: string; to?: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -241,13 +278,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
return "Observation feed not configured. Add observationFeed to your plugin config.";
|
return "Observation feed not configured. Add observationFeed to your plugin config.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args[0] === "on") {
|
const arg = ctx.args?.trim();
|
||||||
api.log("[claude-mem] Feed enable requested via command");
|
|
||||||
|
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.";
|
return "Feed enable requested. Update observationFeed.enabled in your plugin config to persist.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args[0] === "off") {
|
if (arg === "off") {
|
||||||
api.log("[claude-mem] Feed disable requested via command");
|
api.logger.info("[claude-mem] Feed disable requested via command");
|
||||||
return "Feed disable requested. Update observationFeed.enabled in your plugin config to persist.";
|
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 logs = [];
|
||||||
|
|
||||||
const mockApi = {
|
const mockApi = {
|
||||||
getConfig: () => ({}),
|
id: "claude-mem",
|
||||||
log: (message) => {
|
name: "Claude-Mem (Persistent Memory)",
|
||||||
logs.push(message);
|
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) => {
|
registerService: (service) => {
|
||||||
registeredService = service;
|
registeredService = service;
|
||||||
|
|||||||
Reference in New Issue
Block a user