Skip to content
Open
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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,13 @@ openclaw plugins install -l /path/to/memory-powermem

**Note:** Running `npm i memory-powermem` in a Node project only adds the package to that project’s `node_modules`; it does **not** register the plugin with OpenClaw. To use this as an OpenClaw plugin, you must run `openclaw plugins install memory-powermem` (or install from a path as above), then restart the gateway.

After install, run `openclaw plugins list` and confirm `memory-powermem` is listed. With **no** `plugins.entries["memory-powermem"].config`, the plugin uses **defaults**: `mode: "cli"`, `envFile` under `~/.openclaw/powermem/powermem.env`, `pmemPath: "pmem"`, plus `autoCapture` / `autoRecall` / `inferOnAdd` enabled. Ensure `pmem` is on PATH (or set `pmemPath`) and the env file exists and is valid.
After install, run `openclaw plugins list` and confirm `memory-powermem` is listed. With **no** `plugins.entries["memory-powermem"].config`, the plugin uses **defaults**: `mode: "cli"`, `pmemPath: "bundled"` (npm `powermem` from plugin dependencies), `useOpenClawModel: true` (SQLite under OpenClaw state + LLM from `agents.defaults.model`), plus `autoCapture` / `autoRecall` / `inferOnAdd` enabled. No separate `powermem.env` is required unless you opt out of OpenClaw model injection.

---

## Step 3: Configure OpenClaw (optional)

If you use **CLI mode** with the default paths and `pmem` on PATH, you can skip this step. Customize for HTTP, a different URL/API key, or a non-default `envFile` / `pmemPath`.
If you use **CLI mode** with defaults (`bundled` + OpenClaw model injection), you can skip this step. Customize for HTTP, a different URL/API key, Python `pmem` (`pmemPath: "auto"` or an absolute path), or a `powermem` `.env` via `envFile`.

**CLI (default):**

Expand All @@ -205,7 +205,7 @@ If you use **CLI mode** with the default paths and `pmem` on PATH, you can skip
"config": {
"mode": "cli",
"envFile": "/home/you/.openclaw/powermem/powermem.env",
"pmemPath": "pmem",
"pmemPath": "bundled",
"autoCapture": true,
"autoRecall": true,
"inferOnAdd": true
Expand All @@ -230,7 +230,7 @@ If you use **CLI mode** with the default paths and `pmem` on PATH, you can skip

Notes:

- **CLI (default):** You may omit `mode` and use CLI when `baseUrl` is empty; use `envFile` + `pmemPath`.
- **CLI (default):** You may omit `mode` and use CLI when `baseUrl` is empty. Default `pmemPath` is `bundled` (npm CLI). Use `envFile` and/or `pmemPath` when you need a custom setup.
- **HTTP:** When `mode` is `http`, `baseUrl` is required; if you set `baseUrl` without `mode`, the plugin treats it as HTTP. Do **not** append `/api/v1` to `baseUrl`. If the server uses API key auth, add `"apiKey"`.
- **Restart the OpenClaw gateway** (or Mac menubar app) after changing config.

Expand Down Expand Up @@ -287,7 +287,7 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway
| `baseUrl` | Yes (http) | PowerMem API base URL when `mode` is `http`, e.g. `http://localhost:8000`, no `/api/v1` suffix. |
| `apiKey` | No | Set when PowerMem server has API key authentication enabled (http mode). |
| `envFile` | No | CLI: path to PowerMem `.env` (default when using plugin defaults: `~/.openclaw/powermem/powermem.env`). |
| `pmemPath` | No | CLI: path to `pmem` executable; default `pmem`. |
| `pmemPath` | No | CLI: `bundled` (default), `auto`, or path/command for `pmem`. |
| `userId` | No | User isolation (multi-user); default `openclaw-user`. |
| `agentId` | No | Agent isolation (multi-agent); default `openclaw-agent`. |
| `autoCapture` | No | Auto-store from conversations after agent ends; default `true`. |
Expand Down Expand Up @@ -320,7 +320,7 @@ Exposed to OpenClaw agents:

**1. `openclaw ltm health` fails or cannot connect**

- **CLI:** `pmem` on PATH or correct `pmemPath`; valid `.env` at `envFile`.
- **CLI:** npm `powermem` installed with the plugin (`bundled`), or correct `pmemPath`; optional `.env` at `envFile` if not using OpenClaw model injection.
- **HTTP:** PowerMem is running (HTTP server in a terminal, or Docker); `baseUrl` is correct (e.g. `http://localhost:8000`; watch for `127.0.0.1` vs `localhost` mismatches).
- Remote server: use the host IP or hostname instead of `localhost`.

Expand Down
14 changes: 8 additions & 6 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,13 @@ openclaw plugins install -l /path/to/memory-powermem

**说明:** 在某个 Node 项目里执行 `npm i memory-powermem` 只会把包装进该项目的 `node_modules`,**不会**在 OpenClaw 里注册插件。若要在 OpenClaw 里使用本插件,必须执行 `openclaw plugins install memory-powermem`(或按上面用本地路径安装),再重启 gateway。

安装成功后,可用 `openclaw plugins list` 确认能看到 `memory-powermem`。若未写 `plugins.entries["memory-powermem"].config`,插件 **默认**:`mode: "cli"`、`envFile` 为 `~/.openclaw/powermem/powermem.env`、`pmemPath: "pmem"`,并开启 `autoCapture`、`autoRecall`、`inferOnAdd`。请确保 `pmem` 在 PATH 上(或配置 `pmemPath`),且上述 `.env` 有效
安装成功后,可用 `openclaw plugins list` 确认能看到 `memory-powermem`。若未写 `plugins.entries["memory-powermem"].config`,插件 **默认**:`mode: "cli"`、`pmemPath: "bundled"`(优先插件旁的 npm `powermem`,否则用 PATH 上的 `pmem`)、`useOpenClawModel: true`(SQLite 在 OpenClaw 状态目录 + 从 `agents.defaults.model` 注入 LLM),并开启 `autoCapture`、`autoRecall`、`inferOnAdd`。若不使用 OpenClaw 注入模型,再准备 `powermem` 的 `.env`(`envFile`)

---

## 第三步:配置 OpenClaw(可选)

若使用 **CLI 默认路径** 且 `pmem` 已在 PATH,可跳过。需要 HTTP、改 URL/API Key、或自定义 `envFile` / `pmemPath` 时再改配置。
若使用 **CLI 默认**(`bundled` + OpenClaw 模型注入),可跳过。需要 HTTP、改 URL/API Key、使用 Python 版 `pmem`(`pmemPath: "auto"` 或绝对路径)、或通过 `envFile` 时再改配置。

**CLI(默认):**

Expand All @@ -206,7 +206,7 @@ openclaw plugins install -l /path/to/memory-powermem
"config": {
"mode": "cli",
"envFile": "/home/you/.openclaw/powermem/powermem.env",
"pmemPath": "pmem",
"pmemPath": "bundled",
"autoCapture": true,
"autoRecall": true,
"inferOnAdd": true
Expand All @@ -231,7 +231,7 @@ openclaw plugins install -l /path/to/memory-powermem

说明:

- **CLI(默认):** 可不写 `mode` 且 `baseUrl` 为空时走 CLI;使用 `envFile` + `pmemPath`。
- **CLI(默认):** 可不写 `mode` 且 `baseUrl` 为空时走 CLI。默认 `pmemPath` 为 `bundled`(npm CLI)。需要时再配 `envFile` / `pmemPath`。
- **HTTP:** `mode` 为 `http` 时必须配置 `baseUrl`;若只写 `baseUrl` 不写 `mode`,插件会按 HTTP 处理。**不要**在 `baseUrl` 上加 `/api/v1`。若服务开了 API Key,加 `"apiKey"`。
- 改完配置后**重启 OpenClaw gateway**(或 Mac 菜单栏应用)。

Expand Down Expand Up @@ -288,13 +288,15 @@ openclaw ltm search "咖啡"
| `baseUrl` | 是(http) | `mode` 为 `http` 时必填,PowerMem API 根地址,如 `http://localhost:8000`,不要带 `/api/v1`。 |
| `apiKey` | 否 | PowerMem 开启 API Key 鉴权时填写(http 模式)。 |
| `envFile` | 否 | CLI:PowerMem `.env`;插件默认约定 `~/.openclaw/powermem/powermem.env`。 |
| `pmemPath` | 否 | CLI 模式:`pmem` 可执行路径,默认 `pmem`。 |
| `pmemPath` | 否 | CLI:`bundled`(默认)、`auto` 或 `pmem` 的路径/命令。 |
| `userId` | 否 | 用于多用户隔离,默认 `openclaw-user`。 |
| `agentId` | 否 | 用于多 Agent 隔离,默认 `openclaw-agent`。 |
| `autoCapture` | 否 | 会话结束后是否自动把对话交给 PowerMem 抽取记忆,默认 `true`。 |
| `autoRecall` | 否 | 会话开始前是否自动注入相关记忆,默认 `true`。 |
| `inferOnAdd` | 否 | 写入时是否用 PowerMem 智能抽取,默认 `true`。 |

**记忆划分与分享:** `userId` / `agentId` 如何配合、与「组」相关的 `scope` / `run_id`、以及本插件始终携带 `agent_id` 的含义,见 [docs/agent-memory-sharing.md](docs/agent-memory-sharing.md)。

**自动抓取**:会话结束时,会把本轮用户/助手文本发给 PowerMem(`infer: true`),由 PowerMem 抽取并落库。每轮最多 3 条,每条约 6000 字符以内。

---
Expand All @@ -321,7 +323,7 @@ openclaw ltm search "咖啡"

**1. `openclaw ltm health` 报错连不上**

- **CLI:** `pmem` 在 PATH 或 `pmemPath` 正确;`envFile` 指向有效 `.env`。
- **CLI:** 插件已安装 npm `powermem`(`bundled`),或 `pmemPath` 正确;未用 OpenClaw 注入时再保证 `envFile`。
- **HTTP:** PowerMem 已启动(方式 A 终端或 Docker);`baseUrl` 正确(本机常用 `http://localhost:8000`,注意与 `127.0.0.1` 一致性问题)。
- 若 OpenClaw 和 PowerMem 不在同一台机器,把 `localhost` 改成 PowerMem 所在机器的 IP 或域名。

Expand Down
84 changes: 48 additions & 36 deletions client-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import { existsSync } from "node:fs";
import { execFileSync } from "node:child_process";
import type { PowerMemConfig } from "./config.js";
import { DEFAULT_PMEM_PATH, type PowerMemConfig } from "./config.js";
import type { PowerMemAddResult, PowerMemSearchResult } from "./client.js";
import { resolvePmemExecutable } from "./resolve-powermem-cli.js";

const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MiB

Expand Down Expand Up @@ -36,50 +37,60 @@ function parseJsonOrThrow<T>(stdout: string, context: string): T {
}
}

/** Normalize CLI add result to PowerMemAddResult[]. */
function normalizeAddOutput(raw: unknown): PowerMemAddResult[] {
function coerceId(v: unknown): string | number {
if (v === null || v === undefined) return "";
if (typeof v === "number" && Number.isFinite(v)) return v;
const s = String(v).trim();
if (s !== "" && /^\d+$/.test(s)) {
const n = Number(s);
if (Number.isSafeInteger(n)) return n;
}
return s;
}

function mapAddRow(r: Record<string, unknown>): PowerMemAddResult {
const idRaw = r.memoryId ?? r.memory_id ?? r.id;
return {
memory_id: coerceId(idRaw),
content: String(r.memory ?? r.content ?? ""),
user_id: (r.userId ?? r.user_id) as string | undefined,
agent_id: (r.agentId ?? r.agent_id) as string | undefined,
metadata: r.metadata as Record<string, unknown> | undefined,
};
}

function mapSearchRow(r: Record<string, unknown>): PowerMemSearchResult {
const idRaw = r.memory_id ?? r.memoryId ?? r.id;
return {
memory_id: coerceId(idRaw),
content: String(r.content ?? r.memory ?? ""),
score: Number(r.score ?? r.similarity ?? 0),
metadata: r.metadata as Record<string, unknown> | undefined,
};
}

/** Normalize CLI add JSON (Python pmem or powermem-ts) to PowerMemAddResult[]. */
export function normalizeAddOutput(raw: unknown): PowerMemAddResult[] {
if (Array.isArray(raw)) {
return raw.map((r) => ({
memory_id: Number((r as Record<string, unknown>).id ?? (r as Record<string, unknown>).memory_id ?? 0),
content: String((r as Record<string, unknown>).memory ?? (r as Record<string, unknown>).content ?? ""),
user_id: (r as Record<string, unknown>).user_id as string | undefined,
agent_id: (r as Record<string, unknown>).agent_id as string | undefined,
metadata: (r as Record<string, unknown>).metadata as Record<string, unknown> | undefined,
}));
return raw.map((r) => mapAddRow(r as Record<string, unknown>));
}
const obj = raw as Record<string, unknown>;
const results = obj?.results ?? obj?.data;
const results = obj?.memories ?? obj?.results ?? obj?.data;
if (Array.isArray(results)) {
return results.map((r: Record<string, unknown>) => ({
memory_id: Number(r.id ?? r.memory_id ?? 0),
content: String(r.memory ?? r.content ?? ""),
user_id: r.user_id as string | undefined,
agent_id: r.agent_id as string | undefined,
metadata: r.metadata as Record<string, unknown> | undefined,
}));
return results.map((r: Record<string, unknown>) => mapAddRow(r));
}
return [];
}

/** Normalize CLI search result to PowerMemSearchResult[]. */
function normalizeSearchOutput(raw: unknown): PowerMemSearchResult[] {
/** Normalize CLI search JSON to PowerMemSearchResult[]. */
export function normalizeSearchOutput(raw: unknown): PowerMemSearchResult[] {
if (Array.isArray(raw)) {
return raw.map((r) => ({
memory_id: Number((r as Record<string, unknown>).memory_id ?? (r as Record<string, unknown>).id ?? 0),
content: String((r as Record<string, unknown>).content ?? (r as Record<string, unknown>).memory ?? ""),
score: Number((r as Record<string, unknown>).score ?? (r as Record<string, unknown>).similarity ?? 0),
metadata: (r as Record<string, unknown>).metadata as Record<string, unknown> | undefined,
}));
return raw.map((r) => mapSearchRow(r as Record<string, unknown>));
}
const obj = raw as Record<string, unknown>;
const results = obj?.results ?? obj?.data ?? obj?.memories;
if (Array.isArray(results)) {
return results.map((r: Record<string, unknown>) => ({
memory_id: Number(r.memory_id ?? r.id ?? 0),
content: String(r.content ?? r.memory ?? ""),
score: Number(r.score ?? r.similarity ?? 0),
metadata: r.metadata as Record<string, unknown> | undefined,
}));
return results.map((r: Record<string, unknown>) => mapSearchRow(r));
}
return [];
}
Expand Down Expand Up @@ -109,7 +120,7 @@ export class PowerMemCLIClient {
const raw = cfg.envFile?.trim();
const resolved = raw && existsSync(raw) ? raw : undefined;
return new PowerMemCLIClient({
pmemPath: cfg.pmemPath ?? "pmem",
pmemPath: resolvePmemExecutable(cfg.pmemPath ?? DEFAULT_PMEM_PATH),
resolvedEnvFile: resolved,
userId,
agentId,
Expand Down Expand Up @@ -155,7 +166,7 @@ export class PowerMemCLIClient {
return this.resolvedEnvFile ? ["--env-file", this.resolvedEnvFile] : [];
}

async health(): Promise<{ status: string }> {
async health(): Promise<{ status: string; error?: string }> {
const argsList = [
...this.envFileArgs(),
"--json",
Expand All @@ -172,8 +183,9 @@ export class PowerMemCLIClient {
try {
await this.run(argsList, "health");
return { status: "healthy" };
} catch {
return { status: "unhealthy" };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { status: "unhealthy", error: msg };
}
}

Expand Down
41 changes: 30 additions & 11 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
import type { PowerMemConfig } from "./config.js";

export type PowerMemSearchResult = {
memory_id: number;
/** String IDs are used by powermem-ts; HTTP API may return numeric ids. */
memory_id: string | number;
content: string;
score: number;
metadata?: Record<string, unknown>;
};

export type PowerMemAddResult = {
memory_id: number;
memory_id: string | number;
content: string;
user_id?: string;
agent_id?: string;
Expand Down Expand Up @@ -99,13 +100,18 @@ export class PowerMemClient {
}

/** GET /api/v1/system/health */
async health(): Promise<{ status: string }> {
const data = await this.request<{ data?: { status?: string } }>(
"GET",
"/api/v1/system/health",
undefined,
);
return { status: data?.data?.status ?? "unknown" };
async health(): Promise<{ status: string; error?: string }> {
try {
const data = await this.request<{ data?: { status?: string } }>(
"GET",
"/api/v1/system/health",
undefined,
);
return { status: data?.data?.status ?? "unknown" };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { status: "unhealthy", error: msg };
}
}

/** POST /api/v1/memories */
Expand All @@ -126,7 +132,13 @@ export class PowerMemClient {
body,
);
if (!res?.data) return [];
return res.data;
return res.data.map((row) => ({
...row,
memory_id:
row.memory_id !== undefined && row.memory_id !== null
? String(row.memory_id)
: row.memory_id,
}));
}

/** POST /api/v1/memories/search */
Expand All @@ -141,7 +153,14 @@ export class PowerMemClient {
success: boolean;
data?: { results?: PowerMemSearchResult[] };
}>("POST", "/api/v1/memories/search", body);
return res?.data?.results ?? [];
const rows = res?.data?.results ?? [];
return rows.map((row) => ({
...row,
memory_id:
row.memory_id !== undefined && row.memory_id !== null
? String(row.memory_id)
: row.memory_id,
}));
}

/** DELETE /api/v1/memories/:memory_id */
Expand Down
14 changes: 11 additions & 3 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ export type PowerMemConfig = {
apiKey?: string;
/** CLI mode: path to .env (optional; pmem discovers if omitted). */
envFile?: string;
/** CLI mode: path to pmem binary (default "pmem"). */
/**
* CLI: how to run `pmem`.
* - `bundled` (default): prefer npm `powermem` next to this plugin; else `pmem` on PATH (e.g. Python).
* - `auto`: same resolution as `bundled`.
* - any other string: command name or absolute path to a `pmem` binary.
*/
pmemPath?: string;
/**
* When true (default), inject LLM/embedding from OpenClaw gateway config into `pmem`
Expand Down Expand Up @@ -71,6 +76,9 @@ const ALLOWED_KEYS = [
"inferOnAdd",
] as const;

/** CLI `pmemPath` when omitted; npm `powermem` bundled with this plugin. */
export const DEFAULT_PMEM_PATH = "bundled";

export const powerMemConfigSchema = {
parse(value: unknown): PowerMemConfig {
if (!value || typeof value !== "object" || Array.isArray(value)) {
Expand Down Expand Up @@ -111,7 +119,7 @@ export const powerMemConfigSchema = {
const pmemPath =
typeof pmemPathRaw === "string" && pmemPathRaw.trim()
? pmemPathRaw.trim()
: "pmem";
: DEFAULT_PMEM_PATH;

return {
mode,
Expand Down Expand Up @@ -178,7 +186,7 @@ export const DEFAULT_PLUGIN_CONFIG: PowerMemConfig = {
mode: "cli",
baseUrl: "",
envFile: undefined,
pmemPath: "pmem",
pmemPath: DEFAULT_PMEM_PATH,
useOpenClawModel: true,
autoCapture: true,
autoRecall: true,
Expand Down
Loading
Loading