Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ on:
push:
tags:
- 'v*'
pull_request:
# Manual trigger for dev: lint, test, and package only; no GitHub Release or npm publish
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or PR ref (e.g. refs/pull/123/merge). Leave empty to run on default branch.'
required: false
type: string

permissions:
contents: write
Expand All @@ -17,6 +23,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
fetch-depth: 0

- name: Setup Node.js
Expand Down
40 changes: 25 additions & 15 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
*/

import { Type } from "@sinclair/typebox";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import type {
OpenClawPluginApi,
OpenClawPluginCliContext,
} from "openclaw/plugin-sdk/memory-core";
import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk";

import {
powerMemConfigSchema,
Expand Down Expand Up @@ -64,7 +68,7 @@ const memoryPlugin = {
Type.Number({ description: "Min score 0–1 to include (default: plugin recallScoreThreshold)" }),
),
}),
async execute(_toolCallId, params) {
async execute(_toolCallId: string, params: Record<string, unknown>) {
const limit =
typeof (params as { limit?: number }).limit === "number"
? Math.max(1, Math.min(100, Math.floor((params as { limit: number }).limit)))
Expand All @@ -73,6 +77,7 @@ const memoryPlugin = {
typeof (params as { scoreThreshold?: number }).scoreThreshold === "number"
? Math.max(0, Math.min(1, (params as { scoreThreshold: number }).scoreThreshold))
: (cfg.recallScoreThreshold ?? 0);
const query = String((params as { query?: string }).query ?? "");

try {
const requestLimit = Math.min(100, Math.max(limit * 2, limit + 10));
Expand Down Expand Up @@ -136,7 +141,7 @@ const memoryPlugin = {
Type.Number({ description: "Importance 0-1 (default: 0.7)" }),
),
}),
async execute(_toolCallId, params) {
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { text, importance = 0.7 } = params as {
text: string;
importance?: number;
Expand Down Expand Up @@ -195,7 +200,7 @@ const memoryPlugin = {
query: Type.Optional(Type.String({ description: "Search to find memory" })),
memoryId: Type.Optional(Type.String({ description: "Specific memory ID" })),
}),
async execute(_toolCallId, params) {
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { query, memoryId } = params as { query?: string; memoryId?: string };

try {
Expand Down Expand Up @@ -277,7 +282,7 @@ const memoryPlugin = {
// ========================================================================

api.registerCli(
({ program }) => {
({ program }: OpenClawPluginCliContext) => {
const ltm = program
.command("ltm")
.description("PowerMem long-term memory plugin commands");
Expand All @@ -287,7 +292,9 @@ const memoryPlugin = {
.description("Search memories")
.argument("<query>", "Search query")
.option("--limit <n>", "Max results", "5")
.action(async (query: string, opts: { limit?: string }) => {
.action(async (...args: unknown[]) => {
const query = String(args[0] ?? "");
const opts = (args[1] ?? {}) as { limit?: string };
const limit = parseInt(opts.limit ?? "5", 10);
const results = await client.search(query, limit);
console.log(JSON.stringify(results, null, 2));
Expand All @@ -310,7 +317,8 @@ const memoryPlugin = {
.command("add")
.description("Manually add a memory (for testing or one-off storage)")
.argument("<text>", "Content to store")
.action(async (text: string) => {
.action(async (...args: unknown[]) => {
const text = String(args[0] ?? "");
try {
const created = await client.add(text.trim(), { infer: cfg.inferOnAdd });
if (created.length === 0) {
Expand All @@ -332,15 +340,16 @@ const memoryPlugin = {
// ========================================================================

if (cfg.autoRecall) {
api.on("before_agent_start", async (event) => {
if (!event.prompt || event.prompt.length < 5) return;
api.on("before_agent_start", async (event: unknown) => {
const e = event as { prompt: string; messages?: unknown[] };
if (!e.prompt || e.prompt.length < 5) return;

const recallLimit = Math.max(1, Math.min(100, cfg.recallLimit ?? 5));
const scoreThreshold = Math.max(0, Math.min(1, cfg.recallScoreThreshold ?? 0));

try {
const requestLimit = Math.min(100, Math.max(recallLimit * 2, recallLimit + 10));
const raw = await client.search(event.prompt, requestLimit);
const raw = await client.search(e.prompt, requestLimit);
const results = raw
.filter((r) => (r.score ?? 0) >= scoreThreshold)
.slice(0, recallLimit);
Expand All @@ -360,14 +369,15 @@ const memoryPlugin = {
}

if (cfg.autoCapture) {
api.on("agent_end", async (event) => {
if (!event.success || !event.messages || event.messages.length === 0) {
api.on("agent_end", async (event: unknown) => {
const e = event as { messages: unknown[]; success: boolean; error?: string };
if (!e.success || !e.messages || e.messages.length === 0) {
return;
}

try {
const texts: string[] = [];
for (const msg of event.messages) {
for (const msg of e.messages) {
if (!msg || typeof msg !== "object") continue;
const msgObj = msg as Record<string, unknown>;
const role = msgObj.role;
Expand Down Expand Up @@ -433,7 +443,7 @@ const memoryPlugin = {

api.registerService({
id: "memory-powermem",
start: async (_ctx) => {
start: async (_ctx: OpenClawPluginServiceContext) => {
try {
const h = await client.health();
const where = cfg.mode === "cli" ? `cli ${cfg.pmemPath ?? "pmem"}` : cfg.baseUrl;
Expand All @@ -450,7 +460,7 @@ const memoryPlugin = {
);
}
},
stop: (_ctx) => {
stop: (_ctx: OpenClawPluginServiceContext) => {
api.logger.info("memory-powermem: stopped");
},
});
Expand Down
53 changes: 53 additions & 0 deletions openclaw-plugin-sdk.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Minimal type declarations for openclaw plugin-sdk so the extension type-checks
* without installing openclaw (faster CI). Runtime resolves via openclaw's loader.
*/
declare module "openclaw/plugin-sdk/memory-core" {
type CommandChain = {
command: (name: string, description?: string) => CommandChain;
description: (desc: string) => CommandChain;
argument: (name: string, description?: string) => CommandChain;
option: (flags: string, description?: string, defaultValue?: string) => CommandChain;
action: (fn: (...args: unknown[]) => void | Promise<void>) => CommandChain;
};

export type OpenClawPluginCliContext = {
program: {
command: (name: string, description?: string) => CommandChain;
};
config: unknown;
logger: { info: (msg: string) => void; warn: (msg: string) => void; debug?: (msg: string) => void };
};

type ServiceContext = {
config: unknown;
workspaceDir?: string;
stateDir: string;
logger: { info: (msg: string) => void; warn: (msg: string) => void };
};

export type OpenClawPluginApi = {
pluginConfig?: Record<string, unknown>;
logger: { info: (msg: string) => void; warn: (msg: string) => void; debug?: (msg: string) => void };
registerTool: (tool: unknown, opts?: { name?: string; names?: string[] }) => void;
registerCli: (
registrar: (ctx: OpenClawPluginCliContext) => void | Promise<void>,
opts?: { commands?: string[] },
) => void;
on: (hookName: string, handler: (event: unknown) => unknown | Promise<unknown>) => void;
registerService: (service: {
id: string;
start: (ctx: ServiceContext) => void | Promise<void>;
stop?: (ctx: ServiceContext) => void;
}) => void;
};
}

declare module "openclaw/plugin-sdk" {
export type OpenClawPluginServiceContext = {
config: unknown;
workspaceDir?: string;
stateDir: string;
logger: { info: (msg: string) => void; warn: (msg: string) => void };
};
}
77 changes: 77 additions & 0 deletions test/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Tests for PowerMem plugin config parsing and resolvers.
*/
import { describe, it, expect } from "vitest";
import {
powerMemConfigSchema,
resolveUserId,
resolveAgentId,
DEFAULT_USER_ID,
DEFAULT_AGENT_ID,
type PowerMemConfig,
} from "../config.js";

describe("powerMemConfigSchema", () => {
it("parses valid http config with required fields", () => {
const cfg = powerMemConfigSchema.parse({
mode: "http",
baseUrl: "http://localhost:8000",
autoCapture: true,
autoRecall: true,
inferOnAdd: true,
}) as PowerMemConfig;
expect(cfg.mode).toBe("http");
expect(cfg.baseUrl).toBe("http://localhost:8000");
expect(cfg.autoCapture).toBe(true);
expect(cfg.autoRecall).toBe(true);
expect(cfg.inferOnAdd).toBe(true);
expect(cfg.recallLimit).toBe(5);
expect(cfg.recallScoreThreshold).toBe(0);
});

it("parses valid cli config", () => {
const cfg = powerMemConfigSchema.parse({
mode: "cli",
baseUrl: "",
autoCapture: false,
autoRecall: true,
inferOnAdd: false,
}) as PowerMemConfig;
expect(cfg.mode).toBe("cli");
expect(cfg.pmemPath).toBe("pmem");
});

it("rejects non-object config", () => {
expect(() => powerMemConfigSchema.parse(null)).toThrow("memory-powermem config required");
expect(() => powerMemConfigSchema.parse("")).toThrow();
});

it("rejects http mode without baseUrl", () => {
expect(() =>
powerMemConfigSchema.parse({
mode: "http",
baseUrl: "",
autoCapture: true,
autoRecall: true,
inferOnAdd: true,
}),
).toThrow("baseUrl is required when mode is http");
});
});

describe("resolveUserId / resolveAgentId", () => {
it("returns default user/agent when not set", () => {
const cfg = { userId: undefined, agentId: undefined } as PowerMemConfig;
expect(resolveUserId(cfg)).toBe(DEFAULT_USER_ID);
expect(resolveAgentId(cfg)).toBe(DEFAULT_AGENT_ID);
});

it("returns configured user/agent when set", () => {
const cfg = {
userId: "user-1",
agentId: "agent-1",
} as PowerMemConfig;
expect(resolveUserId(cfg)).toBe("user-1");
expect(resolveAgentId(cfg)).toBe("agent-1");
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
"isolatedModules": true,
"types": ["node", "vitest/globals"]
},
"include": ["*.ts", "*.test.ts"],
"include": ["*.ts", "openclaw-plugin-sdk.d.ts", "test/**/*.ts"],
"exclude": ["node_modules"]
}
Loading