From e592ff31a7bbfe96bd1c33b2aff8c7fb239d717d Mon Sep 17 00:00:00 2001 From: Teingi Date: Thu, 19 Mar 2026 19:50:19 +0800 Subject: [PATCH 1/3] fix(install.sh): --workdir parsing and safe paths for multi-instance selection --- install.sh | 20 +++++++++--- skills/install-powermem-memory/SKILL.md | 31 ++++++++++++------- .../config-reference.md | 9 ++++++ 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/install.sh b/install.sh index a3d6db6..4949b8e 100644 --- a/install.sh +++ b/install.sh @@ -32,9 +32,14 @@ PMEM_PATH="pmem" _expect_workdir="" for arg in "$@"; do if [[ -n "$_expect_workdir" ]]; then - OPENCLAW_DIR="$arg" - PLUGIN_DEST="${OPENCLAW_DIR}/extensions/memory-powermem" - _expect_workdir="" + if [[ "$arg" == -* ]]; then + echo "install.sh: Warning: Missing value for --workdir; ignoring." >&2 + _expect_workdir="" + else + OPENCLAW_DIR="$arg" + PLUGIN_DEST="${OPENCLAW_DIR}/extensions/memory-powermem" + _expect_workdir="" + fi continue fi [[ "$arg" == "-y" || "$arg" == "--yes" ]] && INSTALL_YES="1" @@ -52,6 +57,10 @@ for arg in "$@"; do exit 0 } done +if [[ -n "$_expect_workdir" ]]; then + echo "install.sh: ERROR: Option --workdir requires a path." >&2 + exit 1 +fi RED='\033[0;31m' GREEN='\033[0;32m' @@ -88,11 +97,12 @@ detect_openclaw_instances() { [[ "$(basename "$d")" == .openclaw ]] || [[ "$(basename "$d")" == .openclaw-* ]] || continue list+=("$d") done - echo "${list[@]}" + printf '%s\n' "${list[@]}" } select_workdir() { - local instances=($(detect_openclaw_instances)) + local instances=() + mapfile -t instances < <(detect_openclaw_instances) || true if [[ ${#instances[@]} -le 1 ]]; then return 0 fi diff --git a/skills/install-powermem-memory/SKILL.md b/skills/install-powermem-memory/SKILL.md index ba30cf1..6ffc3cb 100644 --- a/skills/install-powermem-memory/SKILL.md +++ b/skills/install-powermem-memory/SKILL.md @@ -27,23 +27,30 @@ This skill folder includes supplementary docs to reference when needed: ## When User Asks to Install +**Important:** PowerMem must be installed and running (or `pmem` available) **before** installing this plugin. The one-liner `install.sh` only installs the OpenClaw plugin; it does **not** install PowerMem. Users who run the script first often see failures because PowerMem is missing. + 1. **Check OpenClaw** Run `openclaw --version`. If not installed, tell the user to install OpenClaw first: `npm install -g openclaw` and `openclaw onboard`. -2. **Check PowerMem** - - **HTTP mode**: User must have PowerMem server running (e.g. `pip install powermem`, create `.env`, then `powermem-server --port 8000`). - - **CLI mode**: User needs `pmem` on PATH (and optionally a PowerMem `.env`). No server required. +2. **Install and verify PowerMem** (do this before installing the plugin) + - **Python**: PowerMem requires **Python 3.10+**. Have the user run `python3 --version`. If older, ask them to upgrade. + - **HTTP mode** + - Install: `pip install powermem` (recommended: use a virtualenv: `python3 -m venv .venv && source .venv/bin/activate` then `pip install powermem`). + - Create `.env`: copy from [PowerMem .env.example](https://github.com/oceanbase/powermem/blob/master/.env.example) or create a minimal one with at least: `DATABASE_PROVIDER=sqlite`, `LLM_PROVIDER`/`LLM_API_KEY`/`LLM_MODEL`, `EMBEDDING_PROVIDER`/`EMBEDDING_API_KEY`/`EMBEDDING_MODEL`/`EMBEDDING_DIMS`. + - Start server **in the directory that contains `.env`**: `powermem-server --host 0.0.0.0 --port 8000`. + - Verify: `curl -s http://localhost:8000/api/v1/system/health` should return OK. + - **CLI mode** + - Install: `pip install powermem` (same as above; ensure the env is activated so `pmem` is on PATH). + - Check: `pmem --version` or `which pmem`. If not found, user may need to activate the venv or use full path to `pmem`. + - Optional: create PowerMem `.env` (e.g. via `pmem config init`) for DB/LLM/Embedding if using intelligent extraction. 3. **Install the plugin** If the user has the repo path: ```bash - openclaw plugins install /path/to/memory-powermem - ``` - Or from GitHub one-liner: - ```bash - curl -fsSL https://raw.githubusercontent.com/ob-labs/memory-powermem/main/install.sh | bash + openclaw plugins install memory-powermem ``` + 4. **Configure OpenClaw** Set memory slot and config. Example (HTTP, local server): ```bash @@ -115,6 +122,8 @@ Restart the gateway after changing the memory slot. | Symptom | Fix | |---------|-----| -| `openclaw ltm health` fails | For HTTP: ensure PowerMem server is running and `baseUrl` is correct. For CLI: ensure `pmem` is on PATH and optional `.env` is valid. | -| Plugin not loaded | Check `plugins.slots.memory` is `memory-powermem` and restart gateway. | -| Add/search returns 500 or empty | Check PowerMem server logs; usually LLM/Embedding API key or model in PowerMem `.env`. | +| **`pip install powermem` fails** | Ensure Python 3.10+ (`python3 --version`). Use a clean venv. On network or build errors, try `pip install powermem --no-build-isolation` or install problematic deps (e.g. `psycopg2-binary`, `pyobvector`) separately first. See [PowerMem repo](https://github.com/oceanbase/powermem) issues if needed. | +| **`pmem` or `powermem-server` not found** | Installed in a virtualenv: activate it (`source .venv/bin/activate`) so they are on PATH. Or run `python -m powermem.cli.main` for CLI and start server via `python -m server.cli.server` (see PowerMem docs). | +| **`openclaw ltm health` fails** | For HTTP: ensure PowerMem server is running and `baseUrl` is correct; run server in the directory that contains `.env`. For CLI: ensure `pmem` is on PATH and optional `.env` is valid. | +| **Plugin not loaded** | Check `plugins.slots.memory` is `memory-powermem` and restart gateway. | +| **Add/search returns 500 or empty** | Check PowerMem server logs; usually missing or wrong LLM/Embedding API key or model in PowerMem `.env`. Ensure `.env` has at least `LLM_*` and `EMBEDDING_*` set for the provider you use. | diff --git a/skills/install-powermem-memory/config-reference.md b/skills/install-powermem-memory/config-reference.md index bbba3b3..4402c50 100644 --- a/skills/install-powermem-memory/config-reference.md +++ b/skills/install-powermem-memory/config-reference.md @@ -4,6 +4,15 @@ Quick reference for this skill folder. See **SKILL.md** for full details. --- +## Installing PowerMem (do this before the plugin) + +- **Python 3.10+** required. Check with `python3 --version`. +- **Install**: `pip install powermem` (prefer inside a virtualenv). +- **HTTP mode**: Create a `.env` (copy from [PowerMem .env.example](https://github.com/oceanbase/powermem/blob/master/.env.example)), set at least database + LLM + Embedding. Start server in that directory: `powermem-server --port 8000`. Verify: `curl -s http://localhost:8000/api/v1/system/health`. +- **CLI mode**: Ensure `pmem` is on PATH (e.g. activate the venv where powermem is installed). Optional: `pmem config init` for `.env`. + +--- + ## Plugin configuration | Key | Default | Description | From ca9e0170d17969ffab5760b5ce777426c81f3cf4 Mon Sep 17 00:00:00 2001 From: Teingi Date: Mon, 23 Mar 2026 14:45:31 +0800 Subject: [PATCH 2/3] feat(memory-powermem): CLI-first defaults, OpenClaw-driven LLM/SQLite, quickstart skill --- INSTALL.md | 75 +++-- README.md | 74 +++-- README_CN.md | 68 +++-- client-cli.ts | 125 ++++++--- config.ts | 32 ++- index.ts | 64 ++++- install.sh | 73 ++++- openclaw-plugin-sdk.d.ts | 11 + openclaw-powermem-env.ts | 263 ++++++++++++++++++ openclaw.plugin.json | 14 +- package.json | 3 +- skills/install-powermem-memory/SKILL.md | 136 +++++---- .../config-reference.md | 56 ++-- .../install-powermem-memory/powermem-intro.md | 52 ++-- skills/powermem-memory-quickstart/SKILL.md | 76 +++++ test/config.test.ts | 29 ++ test/openclaw-powermem-env.test.ts | 70 +++++ 17 files changed, 984 insertions(+), 237 deletions(-) create mode 100644 openclaw-powermem-env.ts create mode 100644 skills/powermem-memory-quickstart/SKILL.md create mode 100644 test/openclaw-powermem-env.test.ts diff --git a/INSTALL.md b/INSTALL.md index 05368c3..2ebf344 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,9 @@ Give [OpenClaw](https://github.com/openclaw/openclaw) long-term memory via [Powe ## One-Click Install (Linux / macOS) -**Prerequisites:** OpenClaw installed (`openclaw --version`). PowerMem is **not** installed by this script—you either run a PowerMem server yourself (HTTP mode) or use the `pmem` CLI (CLI mode). The script only deploys the plugin and configures OpenClaw. +**Prerequisites:** OpenClaw installed (`openclaw --version`). + +**Default path:** The script configures **CLI mode** (no `powermem-server`). With current plugin defaults you **do not need** `powermem.env`: the plugin injects **SQLite** under your OpenClaw state directory and **LLM/embedding** from OpenClaw `agents.defaults.model` + provider keys (same as the gateway). The script may still create `~/.openclaw/powermem/powermem.env` as an optional override template. You still need `pip install powermem` and `pmem` on PATH (or `pmemPath`). ```bash curl -fsSL https://raw.githubusercontent.com/ob-labs/memory-powermem/main/install.sh | bash @@ -19,7 +21,7 @@ cd /path/to/memory-powermem bash install.sh ``` -Non-interactive (defaults: HTTP mode, baseUrl http://localhost:8000): +Non-interactive (defaults: **CLI** mode, env file `~/.openclaw/powermem/powermem.env`, SQLite template if new): ```bash curl -fsSL https://raw.githubusercontent.com/ob-labs/memory-powermem/main/install.sh | bash -s -y @@ -31,15 +33,33 @@ Install to a specific OpenClaw instance: curl -fsSL ... | bash -s -- --workdir ~/.openclaw-second ``` -The script will: 1) check OpenClaw, 2) ask mode (http / cli) and connection details, 3) deploy the plugin into `~/.openclaw/extensions/memory-powermem`, 4) run `npm install` there, 5) set OpenClaw config (plugins.enabled, slots.memory, entries.memory-powermem). +The script will: 1) resolve OpenClaw workdir, 2) ask mode (**cli** / http) and paths, 3) for CLI, seed `powermem.env` if absent, 4) deploy the plugin into `/extensions/memory-powermem`, 5) run `npm install` there, 6) set OpenClaw config (plugins.enabled, slots.memory, entries.memory-powermem). + +**After running (CLI):** Ensure `pmem` is on PATH (or set `pmemPath`), then `openclaw gateway` and `openclaw ltm health`. With plugin defaults you normally **do not** need to edit `powermem.env`—LLM keys come from OpenClaw. Optional template file may still be created under `~/.openclaw/powermem/`. -**After running:** Start or ensure PowerMem is running (HTTP: `powermem-server --port 8000` in a dir with `.env`; CLI: `pmem` on PATH and optional `.env`). Then start OpenClaw: `openclaw gateway`. +**After running (HTTP / enterprise):** Start PowerMem in a directory with `.env`, then `openclaw gateway`. --- ## Quick Start (Let OpenClaw Install It) -Copy the skill file into OpenClaw’s skill directory, then ask OpenClaw to do the rest. +Copy a **skill** into OpenClaw’s skills directory, then ask OpenClaw to follow it. + +### Easiest (C端 / personal users) + +Use the **minimal** skill—short steps, no `powermem.env` required: + +**Linux / macOS:** + +```bash +mkdir -p ~/.openclaw/skills/powermem-memory-quickstart +cp /path/to/memory-powermem/skills/powermem-memory-quickstart/SKILL.md \ + ~/.openclaw/skills/powermem-memory-quickstart/ +``` + +Then say e.g. **「PowerMem 快速安装」** or **“PowerMem quickstart”**. + +### Full install guide (more options & troubleshooting) **Linux / macOS:** @@ -57,7 +77,7 @@ Copy-Item "path\to\memory-powermem\skills\install-powermem-memory\SKILL.md" ` "$env:USERPROFILE\.openclaw\skills\install-powermem-memory\" ``` -Then tell OpenClaw: **「安装 PowerMem 记忆」** or **“Install PowerMem memory”** — it will read the skill and guide you through setup (install plugin, configure, start PowerMem if needed). +Then say **「安装 PowerMem 记忆」** or **“Install PowerMem memory”**. For manual installation, continue below. @@ -68,26 +88,27 @@ For manual installation, continue below. | Component | Purpose | |-------------|---------| | **OpenClaw** | CLI + gateway; run `openclaw --version` and `openclaw onboard` if needed. | -| **PowerMem** | Either (1) **HTTP**: run `powermem-server` (pip or Docker) and have a base URL, or (2) **CLI**: have `pmem` on PATH and a PowerMem `.env` (optional). | +| **PowerMem** | **CLI (recommended):** `pip install powermem`, `pmem` on PATH, `.env` at `~/.openclaw/powermem/powermem.env` (install script can create a template). **HTTP:** run `powermem-server` and set plugin `mode: http` + `baseUrl`. | -You do **not** need to install PowerMem inside OpenClaw; the plugin talks to an existing server or runs `pmem` locally. +You do **not** install PowerMem inside OpenClaw; the plugin runs `pmem` subprocesses (CLI) or calls a HTTP API (server). --- ## Manual Installation Steps -### 1. Install and start PowerMem (if using HTTP mode) - -See [README.md](README.md#step-1-install-and-start-powermem): install with `pip install powermem` or Docker, create `.env` (LLM + Embedding), then: +### 1. Install PowerMem (CLI first) ```bash -cd /path/to/dir/with/.env -powermem-server --host 0.0.0.0 --port 8000 +python3 -m venv ~/.openclaw/powermem/.venv +source ~/.openclaw/powermem/.venv/bin/activate +pip install powermem ``` -Verify: `curl -s http://localhost:8000/api/v1/system/health` +Create or edit `~/.openclaw/powermem/powermem.env` (see [PowerMem .env.example](https://github.com/oceanbase/powermem/blob/master/.env.example)). Minimal fields: `DATABASE_PROVIDER=sqlite`, `SQLITE_PATH` (absolute path recommended), `LLM_*`, `EMBEDDING_*`. -(If using **CLI mode** only, ensure `pmem` is on PATH and optionally set `POWERMEM_ENV_FILE` or use `--env-file`; the plugin will call `pmem` for each request.) +Verify: `pmem --version` (with venv activated). + +**(Optional) HTTP mode:** install PowerMem, put `.env` in a working directory, run `powermem-server --host 0.0.0.0 --port 8000`, verify `curl -s http://localhost:8000/api/v1/system/health`. ### 2. Install the plugin into OpenClaw @@ -101,9 +122,9 @@ Confirm: `openclaw plugins list` shows `memory-powermem`. ### 3. Configure OpenClaw -Edit `~/.openclaw/openclaw.json` (or set via `openclaw config set`): +Edit `~/.openclaw/openclaw.json` (or set via `openclaw config set`). -**HTTP mode:** +**CLI mode (default, no server):** ```json { @@ -114,8 +135,9 @@ Edit `~/.openclaw/openclaw.json` (or set via `openclaw config set`): "memory-powermem": { "enabled": true, "config": { - "mode": "http", - "baseUrl": "http://localhost:8000", + "mode": "cli", + "envFile": "/home/you/.openclaw/powermem/powermem.env", + "pmemPath": "pmem", "autoCapture": true, "autoRecall": true, "inferOnAdd": true @@ -126,19 +148,22 @@ Edit `~/.openclaw/openclaw.json` (or set via `openclaw config set`): } ``` -**CLI mode (no server):** +Use your real home path for `envFile`. If `pmem` is only inside a venv, set `pmemPath` to the absolute path of the `pmem` binary. + +**HTTP mode (shared / enterprise):** ```json "config": { - "mode": "cli", - "envFile": "/path/to/powermem/.env", - "pmemPath": "pmem", + "mode": "http", + "baseUrl": "http://localhost:8000", "autoCapture": true, "autoRecall": true, "inferOnAdd": true } ``` +If you omit `mode` but set a non-empty `baseUrl`, the plugin treats the backend as **http** (backward compatible). + ### 4. Restart and verify Restart the OpenClaw gateway (or app), then: @@ -171,8 +196,8 @@ OPENCLAW_STATE_DIR=~/.openclaw-second openclaw config set plugins.slots.memory m | Symptom | Fix | |--------|-----| -| `openclaw ltm health` fails | PowerMem server not running or wrong `baseUrl`; for CLI, check `pmem` on PATH and `.env`. | +| `openclaw ltm health` fails | **CLI:** `pmem` not on PATH or wrong `pmemPath`; fix `.env` keys. **HTTP:** server down or wrong `baseUrl`. | | Plugin not loaded | Ensure `plugins.slots.memory` is `memory-powermem` and gateway restarted. | -| Add/search returns 500 or empty | Check PowerMem logs; usually LLM/Embedding API key or model in `.env`. | +| Add/search returns 500 or empty | Check PowerMem logs; usually missing `LLM_*` / `EMBEDDING_*` in `.env`. | More: [README.md#troubleshooting](README.md#troubleshooting). diff --git a/README.md b/README.md index 9ca764f..04edebb 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,49 @@ # OpenClaw Memory (PowerMem) Plugin -This plugin lets [OpenClaw](https://github.com/openclaw/openclaw) use long-term memory via the [PowerMem](https://github.com/oceanbase/powermem) HTTP API: intelligent extraction, Ebbinghaus forgetting curve, multi-agent isolation. +This plugin lets [OpenClaw](https://github.com/openclaw/openclaw) use long-term memory via [PowerMem](https://github.com/oceanbase/powermem): intelligent extraction, Ebbinghaus forgetting curve, multi-agent isolation. -Follow the steps in order: install and start PowerMem, then install the plugin, configure OpenClaw, and verify. +**Default:** **CLI mode** — the plugin runs `pmem` locally (no `powermem-server`). Use **HTTP mode** when you already run a shared PowerMem API (teams / enterprise). + +Follow the steps in order: install PowerMem, then install the plugin, configure OpenClaw (defaults work for CLI + `~/.openclaw/powermem/powermem.env`), and verify. --- ## Prerequisites - **OpenClaw** installed (CLI + gateway working) -- **PowerMem server**: install and run it separately (choose one of the two methods below) -- For PowerMem’s “intelligent extraction”: configure LLM + Embedding API keys in PowerMem’s `.env` (e.g. Qwen / OpenAI) +- **PowerMem** installed (`pip install powermem`) with `pmem` available — either on PATH when you start the gateway, or via absolute `pmemPath` in plugin config +- **`.env` for PowerMem** with at least database + LLM + Embedding (see [PowerMem `.env.example`](https://github.com/oceanbase/powermem/blob/master/.env.example)); for individuals, prefer `~/.openclaw/powermem/powermem.env` and SQLite --- ## Step 1: Install and start PowerMem -Choose **Option A (pip)** or **Option B (Docker)**. +Choose **Option C (CLI, recommended for OpenClaw)** or **Option A (HTTP / pip)** or **Option B (Docker)**. + +### Option C: CLI + SQLite (recommended for individuals) + +No HTTP server. Matches the plugin’s **default** (`mode: cli`). + +1. **Install PowerMem** (venv recommended): + + ```bash + python3 -m venv ~/.openclaw/powermem/.venv + source ~/.openclaw/powermem/.venv/bin/activate + pip install powermem + ``` + +2. **Config** — Use [INSTALL.md](INSTALL.md) one-liner `install.sh` to create `~/.openclaw/powermem/powermem.env` (SQLite template), or copy from PowerMem’s `.env.example`. Set `LLM_*` and `EMBEDDING_*`. + +3. **Plugin / OpenClaw** — After installing the plugin, either leave the default config (CLI + default `envFile` under `~/.openclaw/powermem/`) or set `envFile` / `pmemPath` explicitly if `pmem` is only inside the venv. + +4. **Verify** — With venv activated: `pmem --version`. After gateway start: `openclaw ltm health`. + +--- + +### Option A: HTTP server with pip + +Choose this for a **standalone API** or when not using CLI mode. ### Option A: Install with pip (run server locally) @@ -136,7 +162,8 @@ JSON response means the server is up. API docs: `http://localhost:8000/docs`. ## Install options - **One-click (Linux/macOS):** See [INSTALL.md](INSTALL.md) for `install.sh` (curl or run from repo root). -- **Let OpenClaw install it:** Copy [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) to `~/.openclaw/skills/install-powermem-memory/`, then tell OpenClaw **「安装 PowerMem 记忆」** or **“Install PowerMem memory”**. +- **Let OpenClaw install it (simplest):** Copy [skills/powermem-memory-quickstart/SKILL.md](skills/powermem-memory-quickstart/SKILL.md) to `~/.openclaw/skills/powermem-memory-quickstart/`, then say **「PowerMem 快速安装」** or **“PowerMem quickstart”**. +- **Full skill (options + troubleshooting):** [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) → **「安装 PowerMem 记忆」** / **“Install PowerMem memory”**. - **Manual:** Steps below. --- @@ -158,15 +185,15 @@ 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. The plugin uses **default config** when none is set: `baseUrl: "http://localhost:8000"`, `autoCapture`, `autoRecall`, and `inferOnAdd` enabled — so you do not need to edit `~/.openclaw/openclaw.json` for the typical setup (PowerMem on localhost:8000). +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. --- ## Step 3: Configure OpenClaw (optional) -If you use PowerMem at **http://localhost:8000** with the default options, skip this step. To **customize** (e.g. different URL, API key, or CLI mode), edit OpenClaw's config (e.g. `~/.openclaw/openclaw.json`) and add or merge the `plugins` section. +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`. -**Example (JSON):** +**CLI (default):** ```json { @@ -176,7 +203,9 @@ If you use PowerMem at **http://localhost:8000** with the default options, skip "memory-powermem": { "enabled": true, "config": { - "baseUrl": "http://localhost:8000", + "mode": "cli", + "envFile": "/home/you/.openclaw/powermem/powermem.env", + "pmemPath": "pmem", "autoCapture": true, "autoRecall": true, "inferOnAdd": true @@ -187,13 +216,12 @@ If you use PowerMem at **http://localhost:8000** with the default options, skip } ``` -**CLI mode (no server):** To use the PowerMem CLI instead of the HTTP server (same machine, no `powermem-server`), set `"mode": "cli"` and optionally `envFile` / `pmemPath`: +**HTTP (shared server):** ```json "config": { - "mode": "cli", - "envFile": "/path/to/powermem/.env", - "pmemPath": "pmem", + "mode": "http", + "baseUrl": "http://localhost:8000", "autoCapture": true, "autoRecall": true, "inferOnAdd": true @@ -202,8 +230,8 @@ If you use PowerMem at **http://localhost:8000** with the default options, skip Notes: -- **HTTP (default):** `baseUrl` is required; PowerMem HTTP base URL **without** `/api/v1`, e.g. `http://localhost:8000`. If PowerMem has API key auth, add `"apiKey": "your-key"`. -- **CLI:** Set `mode` to `"cli"`. Optional: `envFile` (path to PowerMem `.env`), `pmemPath` (default `pmem`). Requires `pmem` on PATH and a valid PowerMem config (e.g. `.env`). +- **CLI (default):** `mode` optional if omitted and `baseUrl` empty; use `envFile` + `pmemPath`. `pmem` must be runnable with your PowerMem `.env`. +- **HTTP:** `baseUrl` required when `mode` is `http` (or omit `mode` and set `baseUrl` only — inferred as HTTP). No `/api/v1` suffix. Optional `apiKey` if the server uses auth. - **Restart the OpenClaw gateway** (or Mac menubar app) after changing config. --- @@ -213,7 +241,7 @@ Notes: In a terminal: ```bash -# Check PowerMem reachability +# Check PowerMem (CLI: pmem subprocess; HTTP: server) openclaw ltm health ``` @@ -255,11 +283,11 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway | Option | Required | Description | |---------------|----------|-------------| -| `mode` | No | Backend: `"http"` (default) or `"cli"`. Use `cli` to run `pmem` locally without a server. | +| `mode` | No | Backend: `"cli"` (default) or `"http"`. If omitted, non-empty `baseUrl` implies `http`. | | `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 mode: path to PowerMem `.env` file. Optional; pmem discovers if omitted. | -| `pmemPath` | No | CLI mode: path to `pmem` executable; default `pmem`. | +| `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`. | | `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`. | @@ -292,9 +320,9 @@ Exposed to OpenClaw agents: **1. `openclaw ltm health` fails or cannot connect** -- Ensure PowerMem is running (Option A terminal still open, or Docker container up). -- Ensure `baseUrl` matches the real address: use `http://localhost:8000` for local (avoid `127.0.0.1` unless you know it matches). -- If OpenClaw and PowerMem are on different machines, use PowerMem’s host IP or hostname instead of `localhost`. +- **CLI:** `pmem` on PATH or correct `pmemPath`; valid `.env` at `envFile`. +- **HTTP:** PowerMem server running; `baseUrl` matches (e.g. `http://localhost:8000`; avoid mixing `127.0.0.1` vs `localhost` unless intentional). +- Remote server: use the host IP or hostname instead of `localhost`. **2. Add/search returns nothing or 500** diff --git a/README_CN.md b/README_CN.md index ee1ba7f..edc3e68 100644 --- a/README_CN.md +++ b/README_CN.md @@ -10,25 +10,47 @@ 本插件让 [OpenClaw](https://github.com/openclaw/openclaw) 通过 [PowerMem](https://github.com/oceanbase/powermem) 使用长期记忆:智能抽取、艾宾浩斯遗忘曲线、多 Agent 隔离。 -按顺序操作:先安装并启动 PowerMem,再安装插件、配置 OpenClaw,最后验证。 +**默认:CLI 模式** — 插件在本机执行 `pmem`,无需 `powermem-server`。**HTTP 模式** 适合已有共享 PowerMem API 的场景(团队 / 企业)。 + +按顺序操作:先安装 PowerMem,再安装插件、配置 OpenClaw(CLI + `~/.openclaw/powermem/powermem.env` 可零额外配置),最后验证。 --- ## 前置条件 - 已安装 **OpenClaw**(CLI + gateway 能正常用) -- **PowerMem 服务**:需要单独安装并启动(见下文两种方式,任选其一) -- 若用 PowerMem 的「智能抽取」:需在 PowerMem 的 `.env` 里配置好 LLM + Embedding 的 API Key(如通义千问 / OpenAI) +- 已 `pip install powermem`,启动 gateway 时 `pmem` 在 PATH 上,或在插件里配置绝对路径 `pmemPath` +- PowerMem 的 **`.env`**(至少数据库 + LLM + Embedding),个人用户建议放在 `~/.openclaw/powermem/powermem.env`,数据库可用 SQLite --- ## 第一步:安装并启动 PowerMem -任选 **方式 A(pip)** 或 **方式 B(Docker)**,二选一即可。 +可选 **方式 C(CLI,推荐给 OpenClaw 个人用户)**、**方式 A(HTTP + pip)** 或 **方式 B(Docker)**。 + +### 方式 C:CLI + SQLite(推荐给个人) + +不跑 HTTP 服务,与插件**默认**配置一致(`mode: cli`)。 + +1. 安装(建议 venv): + + ```bash + python3 -m venv ~/.openclaw/powermem/.venv + source ~/.openclaw/powermem/.venv/bin/activate + pip install powermem + ``` + +2. 配置:用 [INSTALL.md](INSTALL.md) 里的一键 `install.sh` 生成 `~/.openclaw/powermem/powermem.env`(SQLite 模板),或复制 PowerMem 官方 `.env.example`,填写 `LLM_*`、`EMBEDDING_*`。 + +3. 若 `pmem` 只在 venv 里,在插件 `config` 里把 `pmemPath` 设为该 venv 下 `pmem` 的绝对路径。 + +4. 验证:激活 venv 后 `pmem --version`;启动 gateway 后 `openclaw ltm health`。 + +--- -### 方式 A:用 pip 安装(本机跑服务) +### 方式 A:用 pip 安装(本机跑 HTTP 服务) -适合本机已有 Python 3.11+ 的情况。 +适合要**单独起 API 服务**、或不使用 CLI 模式的场景。适合本机已有 Python 3.11+ 的情况。 **1. 安装 PowerMem** @@ -137,7 +159,8 @@ curl -s http://localhost:8000/api/v1/system/health ## 安装方式 - **一键安装(Linux/macOS):** 见 [INSTALL.md](INSTALL.md),使用 `install.sh`(curl 或从仓库根目录执行)。 -- **交给 OpenClaw 安装:** 将 [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) 复制到 `~/.openclaw/skills/install-powermem-memory/`,然后对 OpenClaw 说「**安装 PowerMem 记忆**」。 +- **交给 OpenClaw 安装(最省事):** 将 [skills/powermem-memory-quickstart/SKILL.md](skills/powermem-memory-quickstart/SKILL.md) 复制到 `~/.openclaw/skills/powermem-memory-quickstart/`,然后说「**PowerMem 快速安装**」。 +- **完整说明(排错与进阶):** [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) →「**安装 PowerMem 记忆**」。 - **手动安装:** 按下面步骤操作。 --- @@ -159,15 +182,15 @@ 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`。未在配置中书写本插件 config 时,插件会使用 **默认配置**:`baseUrl: "http://localhost:8000"`,并开启 `autoCapture`、`autoRecall`、`inferOnAdd`,因此典型情况(PowerMem 跑在 localhost:8000)下无需编辑 `~/.openclaw/openclaw.json`。 +安装成功后,可用 `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(可选) -若 PowerMem 在 **http://localhost:8000** 且使用默认选项,可跳过本步。若要 **自定义**(如改 URL、API Key 或使用 CLI 模式),请编辑 OpenClaw 配置文件(如 `~/.openclaw/openclaw.json`),增加或合并 `plugins` 段。 +若使用 **CLI 默认路径** 且 `pmem` 已在 PATH,可跳过。需要 HTTP、改 URL/API Key、或自定义 `envFile` / `pmemPath` 时再改配置。 -**示例(JSON):** +**CLI(默认):** ```json { @@ -177,7 +200,9 @@ openclaw plugins install -l /path/to/memory-powermem "memory-powermem": { "enabled": true, "config": { - "baseUrl": "http://localhost:8000", + "mode": "cli", + "envFile": "/home/you/.openclaw/powermem/powermem.env", + "pmemPath": "pmem", "autoCapture": true, "autoRecall": true, "inferOnAdd": true @@ -188,13 +213,12 @@ openclaw plugins install -l /path/to/memory-powermem } ``` -**CLI 模式(无需起服务):** 若希望用 PowerMem CLI 而不是 HTTP 服务(本机、不跑 powermem-server),可设置 `"mode": "cli"`,并可选填 `envFile` / `pmemPath`: +**HTTP(共享服务):** ```json "config": { - "mode": "cli", - "envFile": "/path/to/powermem/.env", - "pmemPath": "pmem", + "mode": "http", + "baseUrl": "http://localhost:8000", "autoCapture": true, "autoRecall": true, "inferOnAdd": true @@ -203,9 +227,9 @@ openclaw plugins install -l /path/to/memory-powermem 说明: -- **HTTP(默认):** 需填写 `baseUrl`,PowerMem 的 HTTP 地址,**不要**加 `/api/v1`。若 PowerMem 开了 API Key 鉴权,在 `config` 里增加 `"apiKey": "你的key"`。 -- **CLI:** 将 `mode` 设为 `"cli"`。可选:`envFile`(PowerMem 的 `.env` 路径)、`pmemPath`(默认 `pmem`)。需本机已安装 `pmem` 且配置好 PowerMem(如 `.env`)。 -- 改完配置后**重启 OpenClaw gateway**(或重启 Mac 菜单栏应用),配置才会生效。 +- **CLI(默认):** 可不写 `mode` 且 `baseUrl` 为空时走 CLI;使用 `envFile` + `pmemPath`。 +- **HTTP:** `mode` 为 `http` 时必须配置 `baseUrl`;若只写 `baseUrl` 不写 `mode`,插件会按 HTTP 处理。**不要**在 `baseUrl` 上加 `/api/v1`。若服务开了 API Key,加 `"apiKey"`。 +- 改完配置后**重启 OpenClaw gateway**(或 Mac 菜单栏应用)。 --- @@ -256,10 +280,10 @@ openclaw ltm search "咖啡" | 选项 | 必填 | 说明 | |---------------|------|------| -| `mode` | 否 | 后端:`"http"`(默认)或 `"cli"`。选 `cli` 时本机直接跑 `pmem`,无需起服务。 | +| `mode` | 否 | 后端:`"cli"`(默认)或 `"http"`。不写 `mode` 但填了 `baseUrl` 时按 HTTP 处理。 | | `baseUrl` | 是(http) | `mode` 为 `http` 时必填,PowerMem API 根地址,如 `http://localhost:8000`,不要带 `/api/v1`。 | | `apiKey` | 否 | PowerMem 开启 API Key 鉴权时填写(http 模式)。 | -| `envFile` | 否 | CLI 模式:PowerMem `.env` 文件路径;不填则 pmem 自动发现。 | +| `envFile` | 否 | CLI:PowerMem `.env`;插件默认约定 `~/.openclaw/powermem/powermem.env`。 | | `pmemPath` | 否 | CLI 模式:`pmem` 可执行路径,默认 `pmem`。 | | `userId` | 否 | 用于多用户隔离,默认 `openclaw-user`。 | | `agentId` | 否 | 用于多 Agent 隔离,默认 `openclaw-agent`。 | @@ -293,8 +317,8 @@ openclaw ltm search "咖啡" **1. `openclaw ltm health` 报错连不上** -- 确认 PowerMem 已启动(方式 A 的终端还在跑,或 Docker 容器在运行)。 -- 确认 `baseUrl` 与真实地址一致(本机用 `http://localhost:8000`,别写 `127.0.0.1` 除非你确定一致)。 +- **CLI:** `pmem` 在 PATH 或 `pmemPath` 正确;`envFile` 指向有效 `.env`。 +- **HTTP:** PowerMem 已启动(方式 A 终端或 Docker);`baseUrl` 正确(本机常用 `http://localhost:8000`,注意与 `127.0.0.1` 一致性问题)。 - 若 OpenClaw 和 PowerMem 不在同一台机器,把 `localhost` 改成 PowerMem 所在机器的 IP 或域名。 **2. 写入/搜索没反应或报 500** diff --git a/client-cli.ts b/client-cli.ts index 6491d45..f840159 100644 --- a/client-cli.ts +++ b/client-cli.ts @@ -4,6 +4,7 @@ * Use when mode is "cli" (no HTTP server required). */ +import { existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; import type { PowerMemConfig } from "./config.js"; import type { PowerMemAddResult, PowerMemSearchResult } from "./client.js"; @@ -12,9 +13,15 @@ const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MiB export type PowerMemCLIClientOptions = { pmemPath: string; - envFile?: string; + /** Path passed to pmem only if the file exists on disk. */ + resolvedEnvFile?: string; userId: string; agentId: string; + /** + * Vars merged into the subprocess environment (after process.env). + * OpenClaw + SQLite defaults; cached for the plugin process lifetime. + */ + buildProcessEnv?: () => Promise>; }; function parseJsonOrThrow(stdout: string, context: string): T { @@ -79,52 +86,91 @@ function normalizeSearchOutput(raw: unknown): PowerMemSearchResult[] { export class PowerMemCLIClient { private readonly pmemPath: string; - private readonly envFile?: string; + private readonly resolvedEnvFile?: string; private readonly userId: string; private readonly agentId: string; + private readonly buildProcessEnv?: () => Promise>; + private injectPromise: Promise> | null = null; constructor(options: PowerMemCLIClientOptions) { this.pmemPath = options.pmemPath; - this.envFile = options.envFile; + this.resolvedEnvFile = options.resolvedEnvFile; this.userId = options.userId; this.agentId = options.agentId; + this.buildProcessEnv = options.buildProcessEnv; } - static fromConfig(cfg: PowerMemConfig, userId: string, agentId: string): PowerMemCLIClient { + static fromConfig( + cfg: PowerMemConfig, + userId: string, + agentId: string, + extras?: { buildProcessEnv?: () => Promise> }, + ): PowerMemCLIClient { + const raw = cfg.envFile?.trim(); + const resolved = raw && existsSync(raw) ? raw : undefined; return new PowerMemCLIClient({ pmemPath: cfg.pmemPath ?? "pmem", - envFile: cfg.envFile, + resolvedEnvFile: resolved, userId, agentId, + buildProcessEnv: extras?.buildProcessEnv, }); } - private run(args: string[], context: string): string { + private async getInjectedEnv(): Promise> { + if (!this.buildProcessEnv) return {}; + if (!this.injectPromise) { + this.injectPromise = this.buildProcessEnv().catch((err) => { + this.injectPromise = null; + throw err; + }); + } + return this.injectPromise; + } + + private async run(args: string[], context: string): Promise { + const inject = await this.getInjectedEnv(); + const env: NodeJS.ProcessEnv = { ...process.env, ...inject }; + if (this.resolvedEnvFile) { + env.POWERMEM_ENV_FILE = this.resolvedEnvFile; + } try { const out = execFileSync(this.pmemPath, args, { encoding: "utf-8", maxBuffer: DEFAULT_MAX_BUFFER, - env: this.envFile ? { ...process.env, POWERMEM_ENV_FILE: this.envFile } : process.env, + env, }); return out; } catch (err) { const msg = err instanceof Error ? err.message : String(err); - const stderr = err && typeof err === "object" && "stderr" in err ? String((err as { stderr: unknown }).stderr) : ""; + const stderr = + err && typeof err === "object" && "stderr" in err + ? String((err as { stderr: unknown }).stderr) + : ""; throw new Error(`${context}: ${msg}${stderr ? ` ${stderr}` : ""}`); } } + private envFileArgs(): string[] { + return this.resolvedEnvFile ? ["--env-file", this.resolvedEnvFile] : []; + } + async health(): Promise<{ status: string }> { const argsList = [ - ...(this.envFile ? ["--env-file", this.envFile] : []), - "--json", "-j", - "memory", "list", - "--user-id", this.userId, - "--agent-id", this.agentId, - "--limit", "1", + ...this.envFileArgs(), + "--json", + "-j", + "memory", + "list", + "--user-id", + this.userId, + "--agent-id", + this.agentId, + "--limit", + "1", ]; try { - this.run(argsList, "health"); + await this.run(argsList, "health"); return { status: "healthy" }; } catch { return { status: "unhealthy" }; @@ -136,12 +182,16 @@ export class PowerMemCLIClient { options: { infer?: boolean; metadata?: Record } = {}, ): Promise { const args = [ - ...(this.envFile ? ["--env-file", this.envFile] : []), - "--json", "-j", - "memory", "add", + ...this.envFileArgs(), + "--json", + "-j", + "memory", + "add", content, - "--user-id", this.userId, - "--agent-id", this.agentId, + "--user-id", + this.userId, + "--agent-id", + this.agentId, ]; if (options.infer === false) { args.push("--no-infer"); @@ -149,22 +199,27 @@ export class PowerMemCLIClient { if (options.metadata && Object.keys(options.metadata).length > 0) { args.push("--metadata", JSON.stringify(options.metadata)); } - const stdout = this.run(args, "add"); + const stdout = await this.run(args, "add"); const raw = parseJsonOrThrow(stdout, "add"); return normalizeAddOutput(raw); } async search(query: string, limit = 5): Promise { const args = [ - ...(this.envFile ? ["--env-file", this.envFile] : []), - "--json", "-j", - "memory", "search", + ...this.envFileArgs(), + "--json", + "-j", + "memory", + "search", query, - "--user-id", this.userId, - "--agent-id", this.agentId, - "--limit", String(limit), + "--user-id", + this.userId, + "--agent-id", + this.agentId, + "--limit", + String(limit), ]; - const stdout = this.run(args, "search"); + const stdout = await this.run(args, "search"); const raw = parseJsonOrThrow(stdout, "search"); return normalizeSearchOutput(raw); } @@ -172,12 +227,16 @@ export class PowerMemCLIClient { async delete(memoryId: number | string): Promise { const id = String(memoryId); const args = [ - ...(this.envFile ? ["--env-file", this.envFile] : []), - "memory", "delete", id, - "--user-id", this.userId, - "--agent-id", this.agentId, + ...this.envFileArgs(), + "memory", + "delete", + id, + "--user-id", + this.userId, + "--agent-id", + this.agentId, "--yes", ]; - this.run(args, "delete"); + await this.run(args, "delete"); } } diff --git a/config.ts b/config.ts index 05b6cf1..cde55d2 100644 --- a/config.ts +++ b/config.ts @@ -3,6 +3,9 @@ * Validates baseUrl, optional apiKey, and user/agent mapping. */ +import { homedir } from "node:os"; +import { join } from "node:path"; + function assertAllowedKeys( value: Record, allowed: string[], @@ -33,6 +36,11 @@ export type PowerMemConfig = { envFile?: string; /** CLI mode: path to pmem binary (default "pmem"). */ pmemPath?: string; + /** + * When true (default), inject LLM/embedding from OpenClaw gateway config into `pmem` + * (overrides the same keys from an optional .env file). SQLite defaults live under the OpenClaw state dir. + */ + useOpenClawModel?: boolean; userId?: string; agentId?: string; /** Max memories to return in recall / inject in auto-recall. Default 5. */ @@ -53,6 +61,7 @@ const ALLOWED_KEYS = [ "apiKey", "envFile", "pmemPath", + "useOpenClawModel", "userId", "agentId", "recallLimit", @@ -70,8 +79,12 @@ export const powerMemConfigSchema = { const cfg = value as Record; assertAllowedKeys(cfg, [...ALLOWED_KEYS], "memory-powermem config"); + const modeExplicit = + cfg.mode === "cli" || cfg.mode === "http" ? cfg.mode : undefined; + const baseUrlForInfer = + typeof cfg.baseUrl === "string" ? cfg.baseUrl.trim() : ""; const mode = - (cfg.mode === "cli" || cfg.mode === "http" ? cfg.mode : undefined) ?? "http"; + modeExplicit ?? (baseUrlForInfer ? "http" : "cli"); let baseUrl = ""; let apiKey: string | undefined; @@ -106,6 +119,7 @@ export const powerMemConfigSchema = { apiKey, envFile, pmemPath, + useOpenClawModel: cfg.useOpenClawModel !== false, userId: typeof cfg.userId === "string" && cfg.userId.trim() ? cfg.userId.trim() @@ -149,13 +163,23 @@ function toRecallScoreThreshold(v: unknown): number { export const DEFAULT_USER_ID = "openclaw-user"; export const DEFAULT_AGENT_ID = "openclaw-agent"; +/** Canonical PowerMem `.env` path for consumer (CLI) setups; matches install.sh. */ +export function defaultConsumerPowermemEnvPath(): string { + return join(homedir(), ".openclaw", "powermem", "powermem.env"); +} + /** * Default plugin config when openclaw.json has no plugins.entries["memory-powermem"].config. - * Allows "openclaw plugins install memory-powermem" to work without manual config. + * Consumer default: CLI, no .env required — SQLite under OpenClaw state dir + LLM from OpenClaw `agents.defaults.model`. + * Optional: set `envFile` to merge a powermem .env under OpenClaw-derived vars. + * Enterprise / shared server: set `mode: "http"` and `baseUrl`. */ export const DEFAULT_PLUGIN_CONFIG: PowerMemConfig = { - mode: "http", - baseUrl: "http://localhost:8000", + mode: "cli", + baseUrl: "", + envFile: undefined, + pmemPath: "pmem", + useOpenClawModel: true, autoCapture: true, autoRecall: true, inferOnAdd: true, diff --git a/index.ts b/index.ts index 4b06121..98611c9 100644 --- a/index.ts +++ b/index.ts @@ -3,8 +3,8 @@ * * Long-term memory via PowerMem: intelligent extraction, Ebbinghaus * forgetting curve, multi-agent isolation. Supports two backends: - * - HTTP: requires a running PowerMem server (e.g. powermem-server --port 8000). - * - CLI: runs pmem locally (no server); set mode to "cli" and optionally envFile/pmemPath. + * - CLI (default): runs pmem locally; SQLite + LLM from OpenClaw state / agents.defaults.model (optional .env). + * - HTTP: powermem-server (e.g. --port 8000) for shared / enterprise setups. */ import { Type } from "@sinclair/typebox"; @@ -21,8 +21,40 @@ import { resolveAgentId, type PowerMemConfig, } from "./config.js"; +import { homedir } from "node:os"; +import { join } from "node:path"; + import { PowerMemClient } from "./client.js"; import { PowerMemCLIClient } from "./client-cli.js"; +import { + buildDefaultSqlitePowermemEnv, + buildPowermemCliProcessEnv, +} from "./openclaw-powermem-env.js"; + +type GatewayApi = OpenClawPluginApi & { + config?: unknown; + runtime?: { + state?: { resolveStateDir?: (env?: NodeJS.ProcessEnv) => string }; + modelAuth?: { + resolveApiKeyForProvider?: (params: { + provider: string; + cfg?: unknown; + }) => Promise<{ apiKey?: string }>; + }; + }; +}; + +function resolveOpenClawStateDir(api: GatewayApi): string { + const fn = api.runtime?.state?.resolveStateDir; + if (typeof fn === "function") { + try { + return fn(process.env); + } catch { + /* ignore */ + } + } + return join(homedir(), ".openclaw"); +} // ============================================================================ // Plugin Definition @@ -32,11 +64,12 @@ const memoryPlugin = { id: "memory-powermem", name: "Memory (PowerMem)", description: - "PowerMem-backed long-term memory (intelligent extraction, forgetting curve). Backend: HTTP server or local CLI (pmem).", + "PowerMem-backed long-term memory (intelligent extraction, forgetting curve). Default: local CLI (pmem); optional HTTP server for shared deployments.", kind: "memory" as const, configSchema: powerMemConfigSchema, register(api: OpenClawPluginApi) { + const gw = api as GatewayApi; const raw = api.pluginConfig; const toParse = raw && @@ -48,9 +81,30 @@ const memoryPlugin = { const cfg = powerMemConfigSchema.parse(toParse) as PowerMemConfig; const userId = resolveUserId(cfg); const agentId = resolveAgentId(cfg); + const stateDir = resolveOpenClawStateDir(gw); + const buildProcessEnv = + cfg.mode === "cli" + ? cfg.useOpenClawModel !== false + ? () => + buildPowermemCliProcessEnv({ + openclawConfig: gw.config, + stateDir, + resolveProviderAuth: async (provider) => { + const fn = gw.runtime?.modelAuth?.resolveApiKeyForProvider; + if (typeof fn !== "function") return {}; + try { + return await fn({ provider, cfg: gw.config }); + } catch { + return {}; + } + }, + warn: (m) => api.logger.warn(m), + }) + : () => Promise.resolve(buildDefaultSqlitePowermemEnv(stateDir)) + : undefined; const client = cfg.mode === "cli" - ? PowerMemCLIClient.fromConfig(cfg, userId, agentId) + ? PowerMemCLIClient.fromConfig(cfg, userId, agentId, { buildProcessEnv }) : PowerMemClient.fromConfig(cfg, userId, agentId); const modeLabel = cfg.mode === "cli" ? `cli (${cfg.pmemPath ?? "pmem"})` : cfg.baseUrl; @@ -508,7 +562,7 @@ const memoryPlugin = { } catch (err) { const hint = cfg.mode === "cli" - ? "is pmem on PATH and POWERMEM_ENV_FILE or --env-file set?" + ? "is pmem on PATH? Check agents.defaults.model + provider keys in OpenClaw, or set envFile to a powermem .env" : "is PowerMem server running?"; api.logger.warn( `memory-powermem: health check failed (${hint}): ${String(err)}`, diff --git a/install.sh b/install.sh index 4949b8e..7841087 100644 --- a/install.sh +++ b/install.sh @@ -22,11 +22,14 @@ SKIP_OC="${SKIP_OPENCLAW:-0}" HOME_DIR="${HOME:-$USERPROFILE}" OPENCLAW_DIR="${HOME_DIR}/.openclaw" PLUGIN_DEST="${OPENCLAW_DIR}/extensions/memory-powermem" -SELECTED_MODE="http" +# Consumer default: CLI + ~/.openclaw/powermem/powermem.env (no HTTP server). +SELECTED_MODE="cli" BASE_URL="http://localhost:8000" API_KEY="" ENV_FILE="" PMEM_PATH="pmem" +POWMEM_DATA_DIR="" +DEFAULT_POWMEM_ENV="" # Parse args (curl | bash -s -- ...) _expect_workdir="" @@ -49,7 +52,7 @@ for arg in "$@"; do echo " or: bash install.sh [-y] [--workdir ]" echo "" echo "Options:" - echo " -y, --yes Non-interactive (defaults: http, baseUrl http://localhost:8000)" + echo " -y, --yes Non-interactive (defaults: cli, env ~/.openclaw/powermem/powermem.env)" echo " --workdir OpenClaw config dir (default: ~/.openclaw)" echo " -h, --help Show this help" echo "" @@ -126,22 +129,62 @@ select_workdir() { fi } +resolve_powermem_paths() { + mkdir -p "${OPENCLAW_DIR}" + OPENCLAW_DIR="$(cd "${OPENCLAW_DIR}" && pwd)" + PLUGIN_DEST="${OPENCLAW_DIR}/extensions/memory-powermem" + POWMEM_DATA_DIR="${OPENCLAW_DIR}/powermem" + DEFAULT_POWMEM_ENV="${POWMEM_DATA_DIR}/powermem.env" +} + +# Create a minimal SQLite-oriented .env if missing (edit LLM_/EMBEDDING_* before use). +seed_powermem_env_if_missing() { + local target="$1" + [[ -n "$target" ]] || return 0 + mkdir -p "$(dirname "${target}")" "${POWMEM_DATA_DIR}/data" + local sqlite_path="${POWMEM_DATA_DIR}/data/powermem.db" + if [[ -f "${target}" ]]; then + return 0 + fi + info "Creating PowerMem template ${target}" + cat > "${target}" << EOF +TIMEZONE=UTC +DATABASE_PROVIDER=sqlite +SQLITE_PATH=${sqlite_path} +SQLITE_ENABLE_WAL=true +SQLITE_COLLECTION=memories + +LLM_PROVIDER=qwen +LLM_API_KEY= +LLM_MODEL=qwen-plus + +EMBEDDING_PROVIDER=qwen +EMBEDDING_API_KEY= +EMBEDDING_MODEL=text-embedding-v4 +EMBEDDING_DIMS=1536 +EOF +} + select_mode_and_config() { if [[ "$INSTALL_YES" == "1" ]]; then - SELECTED_MODE="http" - BASE_URL="http://localhost:8000" + SELECTED_MODE="cli" + ENV_FILE="${DEFAULT_POWMEM_ENV}" + seed_powermem_env_if_missing "${ENV_FILE}" return 0 fi echo "" - read -r -p "Backend mode: http or cli [http]: " _mode < /dev/tty || true - _mode="${_mode:-http}" + read -r -p "Backend mode: http or cli [cli]: " _mode < /dev/tty || true + _mode="${_mode:-cli}" if [[ "$_mode" == "cli" ]]; then SELECTED_MODE="cli" - read -r -p "Path to PowerMem .env (optional): " ENV_FILE < /dev/tty || true + read -r -p "Path to PowerMem .env [${DEFAULT_POWMEM_ENV}]: " _ef < /dev/tty || true + ENV_FILE="${_ef:-${DEFAULT_POWMEM_ENV}}" + seed_powermem_env_if_missing "${ENV_FILE}" read -r -p "pmem binary path [pmem]: " _pmem < /dev/tty || true PMEM_PATH="${_pmem:-pmem}" else SELECTED_MODE="http" + ENV_FILE="" read -r -p "PowerMem server base URL [http://localhost:8000]: " _url < /dev/tty || true BASE_URL="${_url:-http://localhost:8000}" read -r -p "API Key (optional): " API_KEY < /dev/tty || true @@ -169,7 +212,7 @@ check_openclaw() { deploy_from_repo() { info "Deploying plugin from current directory..." mkdir -p "${PLUGIN_DEST}" - for f in index.ts config.ts client.ts client-cli.ts openclaw.plugin.json package.json tsconfig.json .gitignore; do + for f in index.ts config.ts client.ts client-cli.ts openclaw-powermem-env.ts openclaw.plugin.json package.json tsconfig.json .gitignore; do if [[ -f "$f" ]]; then cp "$f" "${PLUGIN_DEST}/" fi @@ -194,6 +237,7 @@ deploy_from_github() { "config.ts" "client.ts" "client-cli.ts" + "openclaw-powermem-env.ts" "openclaw.plugin.json" "package.json" "tsconfig.json" @@ -236,13 +280,16 @@ configure_openclaw() { "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.autoCapture true --json "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.autoRecall true --json "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.inferOnAdd true --json + "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.useOpenClawModel true --json if [[ "$SELECTED_MODE" == "http" ]]; then "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.baseUrl "${BASE_URL}" [[ -n "$API_KEY" ]] && "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.apiKey "${API_KEY}" else "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.pmemPath "${PMEM_PATH}" - [[ -n "$ENV_FILE" ]] && "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.envFile "${ENV_FILE}" + if [[ -n "$ENV_FILE" ]]; then + "${oc_env[@]}" openclaw config set plugins.entries.memory-powermem.config.envFile "${ENV_FILE}" + fi fi info "OpenClaw plugin configured ✓" @@ -254,6 +301,7 @@ main() { echo "" select_workdir + resolve_powermem_paths info "Target: ${OPENCLAW_DIR}" select_mode_and_config @@ -280,9 +328,10 @@ main() { echo " 2. openclaw gateway" echo " 3. openclaw ltm health" else - echo " 1. Ensure pmem is on PATH (and optional .env)" - echo " 2. openclaw gateway" - echo " 3. openclaw ltm health" + echo " 1. pip install powermem (venv recommended); put pmem on PATH when starting the gateway" + echo " 2. Edit LLM_* and EMBEDDING_* in: ${ENV_FILE:-$DEFAULT_POWMEM_ENV}" + echo " 3. openclaw gateway" + echo " 4. openclaw ltm health" fi echo "" } diff --git a/openclaw-plugin-sdk.d.ts b/openclaw-plugin-sdk.d.ts index e3ef508..f7694d1 100644 --- a/openclaw-plugin-sdk.d.ts +++ b/openclaw-plugin-sdk.d.ts @@ -27,6 +27,17 @@ declare module "openclaw/plugin-sdk/memory-core" { }; export type OpenClawPluginApi = { + /** Full gateway config (OpenClaw ≥ ~2026.3); used to derive LLM keys for memory plugins. */ + config?: unknown; + runtime?: { + state?: { resolveStateDir?: (env?: NodeJS.ProcessEnv) => string }; + modelAuth?: { + resolveApiKeyForProvider?: (params: { + provider: string; + cfg?: unknown; + }) => Promise<{ apiKey?: string }>; + }; + }; pluginConfig?: Record; logger: { info: (msg: string) => void; warn: (msg: string) => void; debug?: (msg: string) => void }; registerTool: (tool: unknown, opts?: { name?: string; names?: string[] }) => void; diff --git a/openclaw-powermem-env.ts b/openclaw-powermem-env.ts new file mode 100644 index 0000000..33c7c41 --- /dev/null +++ b/openclaw-powermem-env.ts @@ -0,0 +1,263 @@ +/** + * Build PowerMem process env from OpenClaw gateway config + model auth. + * Used when no powermem .env exists (or to override LLM keys on top of a file). + */ + +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; + +export type ResolveProviderAuth = (provider: string) => Promise<{ apiKey?: string }>; + +export type BuildPowermemCliEnvOptions = { + openclawConfig: unknown; + stateDir: string; + resolveProviderAuth: ResolveProviderAuth; + warn?: (msg: string) => void; +}; + +function asRecord(v: unknown): Record | undefined { + if (v && typeof v === "object" && !Array.isArray(v)) { + return v as Record; + } + return undefined; +} + +function getPrimaryModelRef(config: unknown): { provider: string; model: string } | undefined { + const c = asRecord(config); + const agents = asRecord(c?.agents); + const defaults = asRecord(agents?.defaults); + const model = defaults?.model; + if (typeof model === "string" && model.includes("/")) { + const i = model.indexOf("/"); + return { provider: model.slice(0, i), model: model.slice(i + 1) }; + } + if (model && typeof model === "object") { + const m = model as Record; + const primary = m.primary; + if (typeof primary === "string" && primary.includes("/")) { + const i = primary.indexOf("/"); + return { provider: primary.slice(0, i), model: primary.slice(i + 1) }; + } + } + return undefined; +} + +function getProviderEntry( + config: unknown, + providerId: string, +): Record | undefined { + const c = asRecord(config); + const models = asRecord(c?.models); + const providers = asRecord(models?.providers); + if (!providers) return undefined; + const raw = providers[providerId]; + return asRecord(raw); +} + +function secretToString(v: unknown): string | undefined { + if (typeof v === "string" && v.trim()) return v.trim(); + return undefined; +} + +/** Map OpenClaw catalog provider id → PowerMem LLM provider name where they differ. */ +function normalizePowermemProvider(openclawProvider: string): string { + const p = openclawProvider.toLowerCase(); + if (p === "google" || p === "google-generative-ai") return "gemini"; + if (p === "dashscope") return "qwen"; + return p; +} + +async function resolveKey( + openclawProvider: string, + providerCfg: Record | undefined, + resolveProviderAuth: ResolveProviderAuth, +): Promise { + try { + const auth = await resolveProviderAuth(openclawProvider); + if (auth.apiKey?.trim()) return auth.apiKey.trim(); + } catch { + /* fall through */ + } + return secretToString(providerCfg?.apiKey); +} + +/** SQLite layout under OpenClaw state dir (no .env file required). */ +export function buildDefaultSqlitePowermemEnv(stateDir: string): Record { + const dataDir = join(stateDir, "powermem", "data"); + try { + mkdirSync(dataDir, { recursive: true }); + } catch { + /* best effort */ + } + return { + TIMEZONE: process.env.TIMEZONE || "UTC", + DATABASE_PROVIDER: "sqlite", + SQLITE_PATH: join(dataDir, "powermem.db"), + SQLITE_ENABLE_WAL: "true", + SQLITE_COLLECTION: "memories", + }; +} + +async function embeddingOpenaiFallback( + resolveProviderAuth: ResolveProviderAuth, + warn?: (msg: string) => void, +): Promise> { + const k = await resolveKey("openai", undefined, resolveProviderAuth).catch(() => undefined); + if (k) { + return { + EMBEDDING_PROVIDER: "openai", + EMBEDDING_API_KEY: k, + EMBEDDING_MODEL: "text-embedding-3-small", + EMBEDDING_DIMS: "1536", + }; + } + warn?.( + "memory-powermem: no OpenAI API key for embeddings; using local Ollama defaults (nomic-embed-text). Install Ollama or add openai provider keys.", + ); + return { + EMBEDDING_PROVIDER: "ollama", + EMBEDDING_MODEL: "nomic-embed-text", + EMBEDDING_DIMS: "768", + OLLAMA_EMBEDDING_BASE_URL: process.env.OLLAMA_HOST || "http://127.0.0.1:11434", + }; +} + +/** + * Async env vars to merge into the `pmem` subprocess (after process.env). + * Always includes SQLite under stateDir. Adds LLM/embedding from OpenClaw when a primary model is set. + */ +export async function buildPowermemCliProcessEnv( + opts: BuildPowermemCliEnvOptions, +): Promise> { + const out: Record = { ...buildDefaultSqlitePowermemEnv(opts.stateDir) }; + const ref = getPrimaryModelRef(opts.openclawConfig); + if (!ref) { + opts.warn?.( + "memory-powermem: OpenClaw agents.defaults.model not set; PowerMem needs LLM env. Configure a model in openclaw.json or use a powermem .env file.", + ); + return out; + } + + const { provider: ocProvider, model } = ref; + const pmProvider = normalizePowermemProvider(ocProvider); + const pCfg = getProviderEntry(opts.openclawConfig, ocProvider); + const baseUrl = typeof pCfg?.baseUrl === "string" ? pCfg.baseUrl.trim() : undefined; + const apiKey = await resolveKey(ocProvider, pCfg, opts.resolveProviderAuth); + + if (!apiKey && pmProvider !== "ollama") { + opts.warn?.( + `memory-powermem: could not resolve API key for provider "${ocProvider}"; set keys in OpenClaw or use a powermem .env file.`, + ); + } + + switch (pmProvider) { + case "openai": { + out.LLM_PROVIDER = "openai"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + if (baseUrl) out.OPENAI_LLM_BASE_URL = baseUrl; + out.EMBEDDING_PROVIDER = "openai"; + if (apiKey) out.EMBEDDING_API_KEY = apiKey; + out.EMBEDDING_MODEL = "text-embedding-3-small"; + out.EMBEDDING_DIMS = "1536"; + if (baseUrl) out.OPENAI_EMBEDDING_BASE_URL = baseUrl; + break; + } + case "groq": { + out.LLM_PROVIDER = "openai"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.OPENAI_LLM_BASE_URL = baseUrl || "https://api.groq.com/openai/v1"; + Object.assign(out, await embeddingOpenaiFallback(opts.resolveProviderAuth, opts.warn)); + break; + } + case "openrouter": { + out.LLM_PROVIDER = "openai"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.OPENAI_LLM_BASE_URL = baseUrl || "https://openrouter.ai/api/v1"; + Object.assign(out, await embeddingOpenaiFallback(opts.resolveProviderAuth, opts.warn)); + break; + } + case "anthropic": { + out.LLM_PROVIDER = "anthropic"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + if (baseUrl) out.ANTHROPIC_LLM_BASE_URL = baseUrl; + Object.assign(out, await embeddingOpenaiFallback(opts.resolveProviderAuth, opts.warn)); + break; + } + case "qwen": { + out.LLM_PROVIDER = "qwen"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.EMBEDDING_PROVIDER = "qwen"; + if (apiKey) out.EMBEDDING_API_KEY = apiKey; + out.EMBEDDING_MODEL = "text-embedding-v4"; + out.EMBEDDING_DIMS = "1536"; + break; + } + case "ollama": { + out.LLM_PROVIDER = "ollama"; + out.LLM_MODEL = model; + out.OLLAMA_LLM_BASE_URL = baseUrl || "http://127.0.0.1:11434"; + out.EMBEDDING_PROVIDER = "ollama"; + out.EMBEDDING_MODEL = "nomic-embed-text"; + out.EMBEDDING_DIMS = "768"; + out.OLLAMA_EMBEDDING_BASE_URL = baseUrl || "http://127.0.0.1:11434"; + break; + } + case "deepseek": { + out.LLM_PROVIDER = "deepseek"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + if (baseUrl) out.DEEPSEEK_LLM_BASE_URL = baseUrl; + Object.assign(out, await embeddingOpenaiFallback(opts.resolveProviderAuth, opts.warn)); + break; + } + case "gemini": { + out.LLM_PROVIDER = "gemini"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.EMBEDDING_PROVIDER = "gemini"; + if (apiKey) out.EMBEDDING_API_KEY = apiKey; + out.EMBEDDING_MODEL = "text-embedding-004"; + out.EMBEDDING_DIMS = "768"; + break; + } + case "siliconflow": { + out.LLM_PROVIDER = "siliconflow"; + if (apiKey) out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.EMBEDDING_PROVIDER = "siliconflow"; + if (apiKey) out.EMBEDDING_API_KEY = apiKey; + out.EMBEDDING_MODEL = "BAAI/bge-m3"; + out.EMBEDDING_DIMS = "1024"; + if (baseUrl) out.SILICONFLOW_EMBEDDING_BASE_URL = baseUrl; + break; + } + default: { + /* OpenAI-compatible custom providers */ + if (baseUrl && apiKey) { + out.LLM_PROVIDER = "openai"; + out.LLM_API_KEY = apiKey; + out.LLM_MODEL = model; + out.OPENAI_LLM_BASE_URL = baseUrl; + out.EMBEDDING_PROVIDER = "openai"; + out.EMBEDDING_API_KEY = apiKey; + out.EMBEDDING_MODEL = "text-embedding-3-small"; + out.EMBEDDING_DIMS = "1536"; + out.OPENAI_EMBEDDING_BASE_URL = baseUrl; + opts.warn?.( + `memory-powermem: treating provider "${ocProvider}" as OpenAI-compatible (custom baseUrl).`, + ); + break; + } + opts.warn?.( + `memory-powermem: unsupported OpenClaw provider "${ocProvider}" for auto PowerMem env; add a powermem .env or open a feature request.`, + ); + } + } + + return out; +} diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 5aea3a3..b961121 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -4,8 +4,8 @@ "uiHints": { "mode": { "label": "Backend mode", - "placeholder": "http", - "help": "Use \"http\" to talk to powermem-server, or \"cli\" to run pmem locally (no server). Default: http" + "placeholder": "cli", + "help": "Use \"cli\" to run pmem locally (recommended for individuals; no server). Use \"http\" for powermem-server (teams / enterprise). Default: cli" }, "baseUrl": { "label": "PowerMem API URL", @@ -22,7 +22,7 @@ "label": "Env file path", "placeholder": "", "advanced": true, - "help": "CLI mode: path to PowerMem .env file. Optional; pmem discovers .env if omitted" + "help": "CLI mode: path to PowerMem .env (default ~/.openclaw/powermem/powermem.env if you use install.sh -y). Optional; pmem can discover .env if omitted" }, "pmemPath": { "label": "pmem binary path", @@ -65,6 +65,11 @@ "inferOnAdd": { "label": "Infer on Add", "help": "Use PowerMem intelligent extraction when adding (infer=true)" + }, + "useOpenClawModel": { + "label": "Use OpenClaw LLM for PowerMem", + "advanced": true, + "help": "When true (default), inject LLM/embedding from OpenClaw agents.defaults.model into pmem (overrides powermem .env for those keys). SQLite defaults under OpenClaw state dir when no .env." } }, "configSchema": { @@ -82,7 +87,8 @@ "recallScoreThreshold": { "type": "number" }, "autoCapture": { "type": "boolean" }, "autoRecall": { "type": "boolean" }, - "inferOnAdd": { "type": "boolean" } + "inferOnAdd": { "type": "boolean" }, + "useOpenClawModel": { "type": "boolean" } }, "required": [] } diff --git a/package.json b/package.json index d394096..b03200c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "memory-powermem", "version": "1.0.0", - "description": "OpenClaw plugin: long-term memory via PowerMem (HTTP or local CLI; intelligent extraction, Ebbinghaus forgetting curve).", + "description": "OpenClaw plugin: long-term memory via PowerMem (default local CLI; optional HTTP server; intelligent extraction, Ebbinghaus forgetting curve).", "type": "module", "main": "index.ts", "exports": { @@ -12,6 +12,7 @@ "config.ts", "client.ts", "client-cli.ts", + "openclaw-powermem-env.ts", "openclaw.plugin.json" ], "scripts": { diff --git a/skills/install-powermem-memory/SKILL.md b/skills/install-powermem-memory/SKILL.md index 6ffc3cb..1107020 100644 --- a/skills/install-powermem-memory/SKILL.md +++ b/skills/install-powermem-memory/SKILL.md @@ -15,115 +15,131 @@ triggers: # PowerMem Memory Guide -This skill folder includes supplementary docs to reference when needed: +This skill folder includes supplementary docs: -- **powermem-intro.md** — Product intro to PowerMem (what it is, core features, relationship with OpenClaw). Use when the user asks "what is PowerMem" or needs an overview. -- **config-reference.md** — Configuration options and common commands quick reference. +- **powermem-intro.md** — What PowerMem is, features, vs file-based memory. +- **config-reference.md** — Config keys, state dir, commands. + +**只想最少步骤的个人用户** → 用隔壁文件夹 **`skills/powermem-memory-quickstart/SKILL.md`**(更短、不写 `powermem.env`)。 ## How It Works -- **Auto-Capture**: At the end of a conversation, the plugin stores valuable user/assistant content into PowerMem, with optional intelligent extraction (infer). -- **Auto-Recall**: Before each turn, it searches for relevant memories and injects them into context. +- **Auto-Capture**: After a conversation, the plugin sends valuable user/assistant text to PowerMem (optional infer / intelligent extraction). +- **Auto-Recall**: Before each turn, it searches memories and can inject a `` block into context. ## When User Asks to Install -**Important:** PowerMem must be installed and running (or `pmem` available) **before** installing this plugin. The one-liner `install.sh` only installs the OpenClaw plugin; it does **not** install PowerMem. Users who run the script first often see failures because PowerMem is missing. +**Recommended order (TO C):** (1) OpenClaw installed and **default model + provider auth** configured. (2) `pip install powermem` and `pmem` available to the gateway (PATH or `pmemPath`). (3) Install the **memory-powermem** plugin. **No `powermem.env` is required** for the default path. + +The curl **`install.sh`** deploys the plugin and OpenClaw entries; with **`-y`** it may still create **`~/.openclaw/powermem/powermem.env`** as an *optional* template—it does **not** run `pip install powermem`. That file is **not** required if the user relies on **OpenClaw-injected** LLM + default SQLite. 1. **Check OpenClaw** - Run `openclaw --version`. If not installed, tell the user to install OpenClaw first: `npm install -g openclaw` and `openclaw onboard`. - -2. **Install and verify PowerMem** (do this before installing the plugin) - - **Python**: PowerMem requires **Python 3.10+**. Have the user run `python3 --version`. If older, ask them to upgrade. - - **HTTP mode** - - Install: `pip install powermem` (recommended: use a virtualenv: `python3 -m venv .venv && source .venv/bin/activate` then `pip install powermem`). - - Create `.env`: copy from [PowerMem .env.example](https://github.com/oceanbase/powermem/blob/master/.env.example) or create a minimal one with at least: `DATABASE_PROVIDER=sqlite`, `LLM_PROVIDER`/`LLM_API_KEY`/`LLM_MODEL`, `EMBEDDING_PROVIDER`/`EMBEDDING_API_KEY`/`EMBEDDING_MODEL`/`EMBEDDING_DIMS`. - - Start server **in the directory that contains `.env`**: `powermem-server --host 0.0.0.0 --port 8000`. - - Verify: `curl -s http://localhost:8000/api/v1/system/health` should return OK. - - **CLI mode** - - Install: `pip install powermem` (same as above; ensure the env is activated so `pmem` is on PATH). - - Check: `pmem --version` or `which pmem`. If not found, user may need to activate the venv or use full path to `pmem`. - - Optional: create PowerMem `.env` (e.g. via `pmem config init`) for DB/LLM/Embedding if using intelligent extraction. - -3. **Install the plugin** - If the user has the repo path: - ```bash - openclaw plugins install memory-powermem - ``` + `openclaw --version`. If missing: `npm install -g openclaw`, `openclaw onboard`. + Ensure **`agents.defaults.model`** is set (e.g. `openai/gpt-4o-mini`) and the corresponding **provider / API key** works for normal chat—the plugin reuses that for PowerMem when **`useOpenClawModel`** is true (default). + +2. **Install PowerMem (CLI — default)** + - Python **3.10+**: `python3 --version`. + - Venv recommended: e.g. `python3 -m venv ~/.openclaw/powermem/.venv && source ~/.openclaw/powermem/.venv/bin/activate`. + - `pip install powermem`. + - **Defaults:** Plugin injects **SQLite** at `/powermem/data/powermem.db` and **LLM + embedding** env vars derived from OpenClaw. Typical `stateDir` is `~/.openclaw` unless the user uses another instance (`OPENCLAW_STATE_DIR`, `--workdir`). + - **Optional `envFile`:** Path to a PowerMem `.env` for extra tuning. If the file **exists**, `pmem` loads it; **OpenClaw-derived vars still override** the same keys when `useOpenClawModel` is true. + - **`useOpenClawModel: false`:** Disables injection; user must supply a **complete** PowerMem config via `.env` and/or environment variables. + - **Verify:** `pmem --version`. If the gateway does not inherit the venv, set **`pmemPath`** to the absolute path of `pmem`. + +3. **HTTP path (enterprise / shared server)** + - `pip install powermem`, `.env` in server working directory, `powermem-server --host 0.0.0.0 --port 8000`. + - Check: `curl -s http://localhost:8000/api/v1/system/health`. + +4. **Install the plugin** + `openclaw plugins install /path/to/memory-powermem`, or **`install.sh`** from [INSTALL.md](https://github.com/ob-labs/memory-powermem/blob/main/INSTALL.md). +5. **Configure OpenClaw** + + **CLI — minimal (recommended, matches plugin defaults):** + Do **not** set `envFile` unless you need a file. Example: -4. **Configure OpenClaw** - Set memory slot and config. Example (HTTP, local server): ```bash openclaw config set plugins.enabled true openclaw config set plugins.slots.memory memory-powermem - openclaw config set plugins.entries.memory-powermem.config.mode http - openclaw config set plugins.entries.memory-powermem.config.baseUrl http://localhost:8000 + openclaw config set plugins.entries.memory-powermem.config.mode cli + openclaw config set plugins.entries.memory-powermem.config.pmemPath pmem + openclaw config set plugins.entries.memory-powermem.config.useOpenClawModel true --json openclaw config set plugins.entries.memory-powermem.config.autoCapture true --json openclaw config set plugins.entries.memory-powermem.config.autoRecall true --json openclaw config set plugins.entries.memory-powermem.config.inferOnAdd true --json ``` - For **CLI mode** (no server): set `mode` to `cli`, and optionally `envFile`, `pmemPath`. -5. **Verify** - Ask the user to restart the gateway, then run: + **CLI — optional `.env` override file:** + + ```bash + openclaw config set plugins.entries.memory-powermem.config.envFile "$HOME/.openclaw/powermem/powermem.env" + ``` + + (Only matters if that path exists; OpenClaw can still override LLM keys when `useOpenClawModel` is true.) + + **HTTP:** + + ```bash + openclaw config set plugins.entries.memory-powermem.config.mode http + openclaw config set plugins.entries.memory-powermem.config.baseUrl http://localhost:8000 + ``` + + Optional: `apiKey` if the server uses auth. + +6. **Verify** + Restart **gateway**, then: + ```bash openclaw ltm health openclaw ltm add "I prefer coffee in the morning" openclaw ltm search "coffee" ``` - If health is OK and search returns the memory, installation succeeded. ## Available Tools | Tool | Description | |------|-------------| -| **memory_recall** | Search long-term memories by query. Params: `query`, optional `limit`, `scoreThreshold`. | -| **memory_store** | Save information (with optional infer). Params: `text`, optional `importance`. | -| **memory_forget** | Delete by `memoryId` or search with `query` then delete. | +| **memory_recall** | Search long-term memories. Params: `query`, optional `limit`, `scoreThreshold`. | +| **memory_store** | Save text; optional infer. Params: `text`, optional `importance`. | +| **memory_forget** | Delete by `memoryId` or by `query` search. | -## Configuration +## Configuration (summary) | Field | Default | Description | |-------|---------|-------------| -| `mode` | `http` | `http` (PowerMem server) or `cli` (local pmem, no server). | -| `baseUrl` | — | Required when mode is http, e.g. `http://localhost:8000`. | -| `apiKey` | — | Optional; for PowerMem server auth. | -| `envFile` | — | CLI mode: path to PowerMem `.env`. | -| `pmemPath` | `pmem` | CLI mode: path to pmem executable. | -| `recallLimit` | `5` | Max memories in recall / auto-recall. | -| `recallScoreThreshold` | `0` | Min score (0–1) to include. | -| `autoCapture` | `true` | Auto-store from conversations. | -| `autoRecall` | `true` | Auto-inject context before reply. | -| `inferOnAdd` | `true` | Use PowerMem intelligent extraction when adding. | +| `mode` | `cli` | `cli` or `http`. | +| `baseUrl` | — | Required for HTTP; if `mode` omitted and `baseUrl` set → HTTP. | +| `apiKey` | — | HTTP server auth. | +| `envFile` | — | Optional CLI `.env` (used only if file exists). | +| `pmemPath` | `pmem` | CLI binary path. | +| `useOpenClawModel` | `true` | Inject LLM/embedding from OpenClaw + default SQLite under state dir. | +| `recallLimit` | `5` | Max memories per recall. | +| `recallScoreThreshold` | `0` | Min score 0–1. | +| `autoCapture` / `autoRecall` / `inferOnAdd` | `true` | Auto memory pipeline and infer on add. | ## Daily Operations ```bash -# Start gateway (after PowerMem server is running for HTTP mode) openclaw gateway -# Check health openclaw ltm health - -# Manual add / search openclaw ltm add "Some fact to remember" openclaw ltm search "query" -# Disable memory slot openclaw config set plugins.slots.memory none - -# Re-enable openclaw config set plugins.slots.memory memory-powermem ``` -Restart the gateway after changing the memory slot. +Restart the gateway after slot or plugin config changes. ## Troubleshooting | Symptom | Fix | |---------|-----| -| **`pip install powermem` fails** | Ensure Python 3.10+ (`python3 --version`). Use a clean venv. On network or build errors, try `pip install powermem --no-build-isolation` or install problematic deps (e.g. `psycopg2-binary`, `pyobvector`) separately first. See [PowerMem repo](https://github.com/oceanbase/powermem) issues if needed. | -| **`pmem` or `powermem-server` not found** | Installed in a virtualenv: activate it (`source .venv/bin/activate`) so they are on PATH. Or run `python -m powermem.cli.main` for CLI and start server via `python -m server.cli.server` (see PowerMem docs). | -| **`openclaw ltm health` fails** | For HTTP: ensure PowerMem server is running and `baseUrl` is correct; run server in the directory that contains `.env`. For CLI: ensure `pmem` is on PATH and optional `.env` is valid. | -| **Plugin not loaded** | Check `plugins.slots.memory` is `memory-powermem` and restart gateway. | -| **Add/search returns 500 or empty** | Check PowerMem server logs; usually missing or wrong LLM/Embedding API key or model in PowerMem `.env`. Ensure `.env` has at least `LLM_*` and `EMBEDDING_*` set for the provider you use. | +| **`pip install powermem` fails** | Python 3.10+, clean venv. See [PowerMem issues](https://github.com/oceanbase/powermem/issues). | +| **`pmem` not found** | Activate venv or set **`pmemPath`** to the full binary. | +| **`openclaw ltm health` unhealthy (CLI)** | Confirm **`agents.defaults.model`** and provider keys in OpenClaw; gateway version should expose plugin **`config`** + **`runtime.modelAuth`**. Or set **`useOpenClawModel: false`** and a full **`envFile`**. | +| **Health OK but add/search errors** | Embedding/LLM mismatch for your provider—see gateway logs; try optional **PowerMem `.env`** from [.env.example](https://github.com/oceanbase/powermem/blob/master/.env.example). | +| **Wrong SQLite file / instance** | Data is under **that OpenClaw instance’s `stateDir`** (`OPENCLAW_STATE_DIR` / `--workdir`). | +| **HTTP mode** | Server running, **`baseUrl`** correct, **`apiKey`** if enabled. | +| **Plugin not loaded** | `plugins.slots.memory` = `memory-powermem`; restart gateway. | diff --git a/skills/install-powermem-memory/config-reference.md b/skills/install-powermem-memory/config-reference.md index 4402c50..395fbf3 100644 --- a/skills/install-powermem-memory/config-reference.md +++ b/skills/install-powermem-memory/config-reference.md @@ -1,15 +1,30 @@ # Config & Commands Quick Reference -Quick reference for this skill folder. See **SKILL.md** for full details. +Quick reference for this skill folder. See **SKILL.md** for the full install flow. --- -## Installing PowerMem (do this before the plugin) +## Before the plugin: PowerMem (Python) -- **Python 3.10+** required. Check with `python3 --version`. -- **Install**: `pip install powermem` (prefer inside a virtualenv). -- **HTTP mode**: Create a `.env` (copy from [PowerMem .env.example](https://github.com/oceanbase/powermem/blob/master/.env.example)), set at least database + LLM + Embedding. Start server in that directory: `powermem-server --port 8000`. Verify: `curl -s http://localhost:8000/api/v1/system/health`. -- **CLI mode**: Ensure `pmem` is on PATH (e.g. activate the venv where powermem is installed). Optional: `pmem config init` for `.env`. +- **Python 3.10+** — `python3 --version`. +- **Install** — `pip install powermem` (virtualenv recommended). +- **CLI (default)** — No `powermem.env` required for a minimal setup: the plugin injects **SQLite** (under the OpenClaw **state directory**) and **LLM + embedding** from OpenClaw (`agents.defaults.model` + provider keys), as long as `useOpenClawModel` is `true` (default). +- **`pmem` on PATH** — When you start `openclaw gateway`, the same environment should expose `pmem`, or set plugin `pmemPath` to the binary’s absolute path (e.g. inside a venv). +- **Optional `.env`** — Set `envFile` to a PowerMem `.env` if you want file-based overrides; if the file exists, it is loaded first, then OpenClaw-derived variables **override** the same keys (when `useOpenClawModel` is true). +- **HTTP (shared server)** — Run `powermem-server` with its own `.env`; plugin `mode: http` + `baseUrl`. Verify with `curl` on `/api/v1/system/health`. + +--- + +## OpenClaw requirements (CLI + auto LLM) + +- Configure **`agents.defaults.model`** (e.g. `openai/gpt-4o-mini`) and provider credentials in **`models.providers`** (and/or auth the way you normally do for the gateway). +- Gateway should expose plugin **`api.config`** and **`api.runtime.modelAuth`** (recent OpenClaw releases, e.g. 2026.3.x). If those are missing, rely on a full **`envFile`** or set **`useOpenClawModel: false`** and supply `LLM_*` / `EMBEDDING_*` yourself. + +--- + +## SQLite data location (CLI, default) + +- **`/powermem/data/powermem.db`**, where `stateDir` is from OpenClaw (`resolveStateDir`), typically `~/.openclaw` unless `OPENCLAW_STATE_DIR` / `--workdir` points elsewhere. --- @@ -17,34 +32,29 @@ Quick reference for this skill folder. See **SKILL.md** for full details. | Key | Default | Description | |-----|---------|-------------| -| `mode` | `http` | `http` (PowerMem server) or `cli` (local pmem) | -| `baseUrl` | — | Required when mode is http, e.g. `http://localhost:8000` | -| `apiKey` | — | Optional; set when PowerMem server has auth enabled | -| `envFile` | — | CLI mode: path to PowerMem `.env` | -| `pmemPath` | `pmem` | CLI mode: path to `pmem` executable | -| `recallLimit` | `5` | Max number of memories per recall | -| `recallScoreThreshold` | `0` | Min score (0–1) to include a memory | -| `autoCapture` | `true` | Auto-store from conversations | -| `autoRecall` | `true` | Auto-inject relevant memories before replying | -| `inferOnAdd` | `true` | Use PowerMem intelligent extraction when adding | +| `mode` | `cli` | `cli` (local `pmem`) or `http` (`powermem-server`). | +| `baseUrl` | — | Required when `mode` is `http` (or omit `mode` and set non-empty `baseUrl` → HTTP). | +| `apiKey` | — | HTTP: optional PowerMem server API key. | +| `envFile` | — | CLI: optional path to PowerMem `.env` (only used if the file exists). | +| `pmemPath` | `pmem` | CLI: `pmem` executable path. | +| `useOpenClawModel` | `true` | Inject LLM/embedding from OpenClaw; default SQLite under state dir. Set `false` to disable injection (you must then provide a full `.env` or env vars). | +| `recallLimit` | `5` | Max memories per recall / auto-recall. | +| `recallScoreThreshold` | `0` | Min score (0–1) to keep a hit. | +| `autoCapture` | `true` | Auto-store from conversations. | +| `autoRecall` | `true` | Auto-inject relevant memories before reply. | +| `inferOnAdd` | `true` | PowerMem intelligent extraction on add. | --- ## Common OpenClaw commands ```bash -# Health check openclaw ltm health - -# Manual add / search openclaw ltm add "Something to remember" openclaw ltm search "query" -# Disable memory slot openclaw config set plugins.slots.memory none - -# Re-enable openclaw config set plugins.slots.memory memory-powermem ``` -Restart the gateway after changing plugin or memory-slot config for changes to take effect. +Restart the gateway after changing plugin or memory-slot config. diff --git a/skills/install-powermem-memory/powermem-intro.md b/skills/install-powermem-memory/powermem-intro.md index 1d5ab0d..0bf64f0 100644 --- a/skills/install-powermem-memory/powermem-intro.md +++ b/skills/install-powermem-memory/powermem-intro.md @@ -6,10 +6,10 @@ Use this doc when the user asks "what is PowerMem", "why use PowerMem", or needs ## What is PowerMem? -**PowerMem** ([GitHub: oceanbase/powermem](https://github.com/oceanbase/powermem)) is a **long-term memory service** that gives AI applications persistent, retrievable memory. It can run as an **HTTP server** (for clients like OpenClaw) or be used via the **CLI** (`pmem`) on the local machine. +**PowerMem** ([GitHub: oceanbase/powermem](https://github.com/oceanbase/powermem)) is a **long-term memory engine** that gives AI applications persistent, retrievable memory. With the **memory-powermem** plugin it is usually used via the **CLI** (`pmem`) on the same machine as OpenClaw; you can instead run an **HTTP server** (`powermem-server`) for shared or team setups. -- **OpenClaw does not bundle Python**: This plugin talks to PowerMem over HTTP API or by invoking `pmem`; the user must install and run PowerMem separately. -- **Data stays on the user's side**: Memories are stored in the user's own database (seekdb, oceanbase, etc.), as configured in PowerMem's `.env`. +- **OpenClaw does not bundle Python**: The plugin runs `pmem` as a subprocess (CLI) or talks to a HTTP API (server mode). Install PowerMem with `pip` (or use Docker for server mode). +- **Data stays on the user’s machine** (typical setup): **SQLite** under the OpenClaw **state directory** (`/powermem/data/powermem.db`) when using plugin defaults—**no `powermem.env` file is required** for that layout. Larger deployments can still use OceanBase, PostgreSQL, etc., configured in PowerMem’s own `.env` or server config. --- @@ -17,42 +17,44 @@ Use this doc when the user asks "what is PowerMem", "why use PowerMem", or needs | Feature | Description | |---------|-------------| -| **Intelligent extraction (Infer)** | When writing memories, can use an LLM to summarize, dedupe, and structure content so only the essential information is stored. Requires LLM + Embedding configured in PowerMem. | -| **Ebbinghaus forgetting curve** | Supports adjusting memory weight or review policy by forgetting curve so important information lasts longer. | -| **Multi-agent / multi-user isolation** | Uses `userId`, `agentId`, etc. to separate memories per user or agent so they do not interfere. | -| **Vector search** | Semantic search over memories (embedding-based) to recall what is most relevant to the current conversation. | +| **Intelligent extraction (Infer)** | On write, an LLM can summarize, dedupe, and structure content. Needs LLM + embedding configured—either injected from **OpenClaw** (default) or from a PowerMem `.env`. | +| **Ebbinghaus forgetting curve** | Adjusts retention / importance so less relevant memories fade over time. | +| **Multi-agent / multi-user isolation** | `userId`, `agentId`, etc. separate namespaces in the store. | +| **Vector search** | Embedding-based semantic search for recall. | --- ## Relationship with OpenClaw -- **OpenClaw**: Provides gateway, sessions, tool dispatch, etc.; its **memory slot** must be implemented by a plugin. -- **memory-powermem**: Implements the memory slot and forwards store/recall/forget requests to PowerMem (HTTP or CLI). -- **PowerMem**: Handles storage, retrieval, intelligent extraction, and forgetting curve; it is where data actually lives. +- **OpenClaw**: Gateway, sessions, tools; the **memory slot** is filled by a plugin. +- **memory-powermem**: Implements that slot and forwards add/search/forget to PowerMem (CLI or HTTP). +- **PowerMem**: Stores data, runs extraction, search, and forgetting logic. -So: the user must **install and run PowerMem first** (or install the `pmem` CLI), then install this plugin and configure the connection (HTTP `baseUrl` or CLI `pmemPath`). +**Typical personal setup:** `pip install powermem`, ensure **`agents.defaults.model`** and provider keys are set in OpenClaw, install the plugin, use **CLI mode** with defaults (`useOpenClawModel: true`, no `envFile`). The plugin supplies SQLite paths and LLM/embedding env to each `pmem` call. + +**Optional:** Point **`envFile`** at a PowerMem `.env` for advanced DB/provider tuning; OpenClaw can still override LLM-related keys when `useOpenClawModel` is true. + +**HTTP mode:** Run `powermem-server`, set plugin `mode: http` and `baseUrl`. --- ## Advantages over OpenClaw file-based memory -OpenClaw can work with **file-as-memory**: storing context in workspace files like `memory/YYYY-MM-DD-slug.md`, `MEMORY.md`, or `memory.md`, and using built-in search over those files. The **session-memory** hook writes a snapshot to a file on `/new` or `/reset`. PowerMem (via this plugin) offers a different model with these advantages: - -| Aspect | File-based (OpenClaw default) | PowerMem + plugin | -|--------|--------------------------------|--------------------| -| **Recall** | Load fixed files (e.g. today + yesterday) or search workspace; relevance is by recency or keyword/embedding over raw text. | **Semantic recall**: only the top‑k most relevant memories are injected per turn, with score threshold and limit. Fewer tokens, more focused context. | -| **Storage** | Append or overwrite Markdown; no automatic deduplication or structure. | **Structured store** in a DB (seekdb/oceanbase) with **intelligent extraction**: LLM can summarize, dedupe, and normalize when adding, so you keep essential facts instead of raw dumps. | -| **Decay / importance** | Files accumulate unless you manually consolidate or prune. | **Ebbinghaus forgetting curve**: importance and retention can be tuned so older or less relevant memories fade appropriately. | -| **Isolation** | Usually one workspace = one user; multi-agent or multi-user requires separate workspaces or conventions. | **userId / agentId**: same PowerMem backend can serve multiple users or agents with isolated namespaces. | -| **Auto-capture / auto-recall** | Session-memory saves on `/new` or `/reset`; the agent must explicitly read and write memory files otherwise. | **Auto-capture** at end of conversation and **auto-recall** before each turn, so memory is updated and injected without extra user or agent steps. | +OpenClaw can use **files as memory** (e.g. `memory/YYYY-MM-DD.md`, `MEMORY.md`). The **session-memory** hook snapshots to disk on `/new` or `/reset`. PowerMem + this plugin differ as follows: -Use this section when the user asks why to use PowerMem instead of (or in addition to) OpenClaw’s file-based memory. +| Aspect | File-based (typical) | PowerMem + plugin | +|--------|----------------------|-------------------| +| **Recall** | Fixed files or workspace search; relevance often by recency or simple search. | **Semantic recall**: top‑k memories per turn with score threshold and limits; fewer tokens. | +| **Storage** | Append/overwrite Markdown; manual cleanup. | **DB-backed** store (default **SQLite** locally) with optional **intelligent extraction**. | +| **Decay / importance** | Manual consolidation. | **Ebbinghaus-style** tuning where supported. | +| **Isolation** | Often one workspace per context. | **`userId` / `agentId`** namespaces on one backend. | +| **Auto flow** | Agent must read/write files unless hooks cover it. | **Auto-capture** after conversations and **auto-recall** before turns (plugin hooks). | --- -## Two Usage Modes +## Two usage modes -- **HTTP mode**: Run `powermem-server` on the host or a server; the OpenClaw plugin calls its API via `baseUrl`. Suited for multi-client, multi-user, or centralized deployment. -- **CLI mode**: No server; the plugin invokes the user's local `pmem` command. Suited for single-machine, lightweight use. +- **CLI mode (default)**: Plugin runs `pmem`; SQLite + LLM often come from **OpenClaw config** (no mandatory `.env`). +- **HTTP mode**: Central `powermem-server`; plugin uses `baseUrl` (and optional `apiKey`). -For full install and configuration steps, see **SKILL.md** in this folder. +Full steps: **SKILL.md** in this folder. diff --git a/skills/powermem-memory-quickstart/SKILL.md b/skills/powermem-memory-quickstart/SKILL.md new file mode 100644 index 0000000..9d1ab7d --- /dev/null +++ b/skills/powermem-memory-quickstart/SKILL.md @@ -0,0 +1,76 @@ +--- +name: powermem-memory-quickstart +description: 最简单的 OpenClaw 长期记忆(PowerMem)安装步骤,面向个人用户;无需单独配置 powermem.env,复用 OpenClaw 里已配好的对话模型。 +triggers: + - "PowerMem 快速安装" + - "PowerMem 最简单安装" + - "怎么装 PowerMem 记忆" + - "OpenClaw 记忆 怎么装" + - "安装长期记忆" + - "Quick install PowerMem" + - "PowerMem quickstart" + - "Easiest PowerMem setup" +--- + +# PowerMem 记忆 · 极简安装(个人用户) + +你只要记住三件事:**OpenClaw 能正常聊天**、**本机装好 PowerMem**、**装上插件**。不用手写 `powermem.env`,记忆用的模型和 Key 会跟你在 OpenClaw 里配的一样。 + +--- + +## 你要做什么(按顺序) + +1. **确认 OpenClaw 已经能用** + 终端执行 `openclaw --version`,并且你已经在 OpenClaw 里配好了**平时对话用的模型**(能正常回复即可)。 + +2. **安装 PowerMem(Python)** + 建议用虚拟环境,然后安装: + ```bash + python3 -m venv ~/.openclaw/powermem/.venv + source ~/.openclaw/powermem/.venv/bin/activate + pip install powermem + ``` + 装好后执行 `pmem --version`,能输出版本就行。 + +3. **让网关能找到 `pmem`** + 如果你启动 `openclaw gateway` 的终端**没有**激活上面的 venv,有两种简单办法二选一: + - 每次开网关前先 `source ~/.openclaw/powermem/.venv/bin/activate`;或 + - 在插件配置里把 **`pmemPath`** 写成 venv 里 `pmem` 的**完整路径**(装完后可用 `which pmem` 查看)。 + +4. **一键装插件(推荐)** + 在 **Mac / Linux** 上执行(需已安装 OpenClaw): + ```bash + curl -fsSL https://raw.githubusercontent.com/ob-labs/memory-powermem/main/install.sh | bash -s -y + ``` + 脚本会把插件放进 OpenClaw,并打开「用 OpenClaw 的模型驱动记忆」等默认选项。 + +5. **重启网关并检查** + ```bash + openclaw gateway + ``` + 另开终端: + ```bash + openclaw ltm health + ``` + 显示健康后试一句: + ```bash + openclaw ltm add "我喜欢喝美式" + openclaw ltm search "咖啡" + ``` + +--- + +## 若某一步失败 + +| 情况 | 怎么办 | +|------|--------| +| `pip install powermem` 报错 | 确认 Python ≥ 3.10;换干净 venv 再试。 | +| `pmem` 找不到 | 激活 venv,或配置 **`pmemPath`** 为绝对路径。 | +| `ltm health` 不健康 | 确认 OpenClaw 里**默认模型**和 API Key 本身能聊天;升级 OpenClaw 到较新版本后再试。 | +| 想要更多选项(多实例、自建服务器等) | 看仓库里 **`skills/install-powermem-memory/SKILL.md`** 或根目录 **INSTALL.md**。 | + +--- + +## 说明(一句话) + +记忆数据默认存在本机 OpenClaw 数据目录下的 SQLite 里;**不需要**你再单独维护一份 PowerMem 的 `.env`,除非你熟悉进阶配置。 diff --git a/test/config.test.ts b/test/config.test.ts index 1f668d0..d76284a 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -8,6 +8,7 @@ import { resolveAgentId, DEFAULT_USER_ID, DEFAULT_AGENT_ID, + DEFAULT_PLUGIN_CONFIG, type PowerMemConfig, } from "../config.js"; @@ -41,6 +42,34 @@ describe("powerMemConfigSchema", () => { expect(cfg.pmemPath).toBe("pmem"); }); + it("infers http when mode omitted but baseUrl is set", () => { + const cfg = powerMemConfigSchema.parse({ + baseUrl: "http://localhost:8000", + autoCapture: true, + autoRecall: true, + inferOnAdd: true, + }) as PowerMemConfig; + expect(cfg.mode).toBe("http"); + expect(cfg.baseUrl).toBe("http://localhost:8000"); + }); + + it("defaults to cli when mode and baseUrl are empty", () => { + const cfg = powerMemConfigSchema.parse({ + baseUrl: "", + autoCapture: true, + autoRecall: true, + inferOnAdd: true, + }) as PowerMemConfig; + expect(cfg.mode).toBe("cli"); + }); + + it("DEFAULT_PLUGIN_CONFIG uses cli without env file and OpenClaw model injection", () => { + expect(DEFAULT_PLUGIN_CONFIG.mode).toBe("cli"); + expect(DEFAULT_PLUGIN_CONFIG.baseUrl).toBe(""); + expect(DEFAULT_PLUGIN_CONFIG.envFile).toBeUndefined(); + expect(DEFAULT_PLUGIN_CONFIG.useOpenClawModel).toBe(true); + }); + it("rejects non-object config", () => { expect(() => powerMemConfigSchema.parse(null)).toThrow("memory-powermem config required"); expect(() => powerMemConfigSchema.parse("")).toThrow(); diff --git a/test/openclaw-powermem-env.test.ts b/test/openclaw-powermem-env.test.ts new file mode 100644 index 0000000..94c6452 --- /dev/null +++ b/test/openclaw-powermem-env.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +import { + buildDefaultSqlitePowermemEnv, + buildPowermemCliProcessEnv, +} from "../openclaw-powermem-env.js"; + +describe("buildDefaultSqlitePowermemEnv", () => { + it("sets sqlite under state dir", () => { + const e = buildDefaultSqlitePowermemEnv("/tmp/memory-powermem-test-state"); + expect(e.DATABASE_PROVIDER).toBe("sqlite"); + expect(e.SQLITE_PATH).toContain("powermem"); + expect(e.SQLITE_PATH).toContain("powermem.db"); + }); +}); + +describe("buildPowermemCliProcessEnv", () => { + it("maps OpenAI primary model using provider config apiKey", async () => { + const env = await buildPowermemCliProcessEnv({ + openclawConfig: { + agents: { defaults: { model: "openai/gpt-4o-mini" } }, + models: { + providers: { + openai: { baseUrl: "https://api.openai.com/v1", apiKey: "sk-from-config" }, + }, + }, + }, + stateDir: "/tmp/memory-powermem-test-state", + resolveProviderAuth: async () => ({}), + }); + expect(env.LLM_PROVIDER).toBe("openai"); + expect(env.LLM_MODEL).toBe("gpt-4o-mini"); + expect(env.LLM_API_KEY).toBe("sk-from-config"); + expect(env.EMBEDDING_PROVIDER).toBe("openai"); + expect(env.EMBEDDING_API_KEY).toBe("sk-from-config"); + }); + + it("prefers resolveProviderAuth over inline apiKey", async () => { + const env = await buildPowermemCliProcessEnv({ + openclawConfig: { + agents: { defaults: { model: "openai/gpt-4o" } }, + models: { + providers: { + openai: { baseUrl: "https://api.openai.com/v1", apiKey: "inline" }, + }, + }, + }, + stateDir: "/tmp/memory-powermem-test-state", + resolveProviderAuth: async () => ({ apiKey: "from-auth" }), + }); + expect(env.LLM_API_KEY).toBe("from-auth"); + }); + + it("maps qwen and shared embedding", async () => { + const env = await buildPowermemCliProcessEnv({ + openclawConfig: { + agents: { defaults: { model: { primary: "qwen/qwen-plus" } } }, + models: { + providers: { + qwen: { baseUrl: "https://dashscope.aliyuncs.com", apiKey: "qk" }, + }, + }, + }, + stateDir: "/tmp/memory-powermem-test-state", + resolveProviderAuth: async () => ({}), + }); + expect(env.LLM_PROVIDER).toBe("qwen"); + expect(env.LLM_MODEL).toBe("qwen-plus"); + expect(env.EMBEDDING_PROVIDER).toBe("qwen"); + }); +}); From 4372ae0cc5056457cb162965c43357bf77956adb Mon Sep 17 00:00:00 2001 From: Teingi Date: Mon, 23 Mar 2026 17:16:31 +0800 Subject: [PATCH 3/3] feat(env): map Bailian to Qwen and native DashScope URL for embeddings --- README.md | 42 ++++++++++++++---------------- README_CN.md | 6 ++--- openclaw-powermem-env.ts | 19 +++++++++++++- test/openclaw-powermem-env.test.ts | 21 +++++++++++++++ 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 04edebb..c6039c2 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ Follow the steps in order: install PowerMem, then install the plugin, configure ## Step 1: Install and start PowerMem -Choose **Option C (CLI, recommended for OpenClaw)** or **Option A (HTTP / pip)** or **Option B (Docker)**. +Choose **Option A (CLI, recommended for OpenClaw individuals)**, **Option B (HTTP + pip)**, or **Option C (Docker)**. -### Option C: CLI + SQLite (recommended for individuals) +### Option A: CLI + SQLite (recommended for individuals) No HTTP server. Matches the plugin’s **default** (`mode: cli`). @@ -42,19 +42,15 @@ No HTTP server. Matches the plugin’s **default** (`mode: cli`). 2. **Config** — Use [INSTALL.md](INSTALL.md) one-liner `install.sh` to create `~/.openclaw/powermem/powermem.env` (SQLite template), or copy from PowerMem’s `.env.example`. Set `LLM_*` and `EMBEDDING_*`. -3. **Plugin / OpenClaw** — After installing the plugin, either leave the default config (CLI + default `envFile` under `~/.openclaw/powermem/`) or set `envFile` / `pmemPath` explicitly if `pmem` is only inside the venv. +3. If `pmem` exists only inside the venv, set `pmemPath` in the plugin `config` to the absolute path of `pmem` in that venv. 4. **Verify** — With venv activated: `pmem --version`. After gateway start: `openclaw ltm health`. --- -### Option A: HTTP server with pip +### Option B: Install with pip (run HTTP server locally) -Choose this for a **standalone API** or when not using CLI mode. - -### Option A: Install with pip (run server locally) - -Best if you already have Python 3.11+. +Use this when you want a **standalone API** or are not using CLI mode. Best if you already have Python 3.11+ on the machine. **1. Install PowerMem** @@ -97,7 +93,7 @@ EMBEDDING_DIMS=1536 EOF ``` -Replace `your_api_key_here` with your Qwen API key and `your_password` with your OceanBase password. For SQLite (simplest local setup). For OpenAI or others, see PowerMem’s [.env.example](https://github.com/oceanbase/powermem/blob/master/.env.example) for `LLM_*` and `EMBEDDING_*`. +Replace `your_api_key_here` with your Tongyi Qwen API key (and set `your_password` and other DB fields as needed for OceanBase). For OpenAI or other providers, see PowerMem’s [.env.example](https://github.com/oceanbase/powermem/blob/master/.env.example) for `LLM_*` and `EMBEDDING_*`. **3. Start the HTTP server** @@ -122,7 +118,7 @@ If you get JSON (e.g. with `"status":"healthy"`), PowerMem is ready. --- -### Option B: Run with Docker (no Python needed) +### Option C: Run with Docker (no Python needed) Best if you have Docker and prefer not to install Python. @@ -139,7 +135,7 @@ Edit `.env` and set at least: - `LLM_API_KEY`, `LLM_PROVIDER`, `LLM_MODEL` - `EMBEDDING_API_KEY`, `EMBEDDING_PROVIDER`, `EMBEDDING_MODEL` -Database can stay default; OceanBase is recommended (see .env.example). +Database: OceanBase is recommended. **2. Start the container** @@ -163,7 +159,7 @@ JSON response means the server is up. API docs: `http://localhost:8000/docs`. - **One-click (Linux/macOS):** See [INSTALL.md](INSTALL.md) for `install.sh` (curl or run from repo root). - **Let OpenClaw install it (simplest):** Copy [skills/powermem-memory-quickstart/SKILL.md](skills/powermem-memory-quickstart/SKILL.md) to `~/.openclaw/skills/powermem-memory-quickstart/`, then say **「PowerMem 快速安装」** or **“PowerMem quickstart”**. -- **Full skill (options + troubleshooting):** [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) → **「安装 PowerMem 记忆」** / **“Install PowerMem memory”**. +- **Full documentation (troubleshooting and advanced topics):** [skills/install-powermem-memory/SKILL.md](skills/install-powermem-memory/SKILL.md) → **「安装 PowerMem 记忆」** / **“Install PowerMem memory”**. - **Manual:** Steps below. --- @@ -173,7 +169,7 @@ JSON response means the server is up. API docs: `http://localhost:8000/docs`. On your machine (use your actual plugin path): ```bash -# Install from npm (recommended for end users; OpenClaw downloads the package from the npm registry) +# Install from npm (recommended for end users; downloads and installs from the official npm registry) openclaw plugins install memory-powermem # Install from a local directory (e.g. cloned repo) @@ -230,8 +226,8 @@ If you use **CLI mode** with the default paths and `pmem` on PATH, you can skip Notes: -- **CLI (default):** `mode` optional if omitted and `baseUrl` empty; use `envFile` + `pmemPath`. `pmem` must be runnable with your PowerMem `.env`. -- **HTTP:** `baseUrl` required when `mode` is `http` (or omit `mode` and set `baseUrl` only — inferred as HTTP). No `/api/v1` suffix. Optional `apiKey` if the server uses auth. +- **CLI (default):** You may omit `mode` and use CLI when `baseUrl` is empty; use `envFile` + `pmemPath`. +- **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. --- @@ -241,7 +237,7 @@ Notes: In a terminal: ```bash -# Check PowerMem (CLI: pmem subprocess; HTTP: server) +# Check whether PowerMem is reachable openclaw ltm health ``` @@ -257,7 +253,7 @@ openclaw ltm add "I prefer a cup of Americano every morning" openclaw ltm search "coffee" ``` -If search returns the line you added (or similar), the full flow (PowerMem → plugin → OpenClaw) is working. +If search returns what you just added (or similar content), the full flow (install PowerMem → install plugin → configure OpenClaw) is working end to end. --- @@ -294,7 +290,7 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway | `autoRecall` | No | Auto-inject relevant memories before agent starts; default `true`. | | `inferOnAdd` | No | Use PowerMem intelligent extraction when adding; default `true`. | -**Auto-capture:** When a conversation ends, user/assistant text is sent to PowerMem with `infer: true`. PowerMem extracts and stores memories. At most 3 chunks per session (each up to 6000 chars). +**Auto-capture:** When a session ends, this round’s user/assistant text is sent to PowerMem (`infer: true`) for extraction and storage. At most 3 items per round, each up to about 6000 characters. --- @@ -303,7 +299,7 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway Exposed to OpenClaw agents: - **memory_recall** — Search long-term memories by query. -- **memory_store** — Save information (with optional infer). +- **memory_store** — Store one memory (optional intelligent extraction on write). - **memory_forget** — Delete by memory ID or by search query. --- @@ -311,7 +307,7 @@ Exposed to OpenClaw agents: ## OpenClaw CLI (when plugin enabled) - `openclaw ltm search [--limit n]` — Search memories. -- `openclaw ltm health` — Check PowerMem server health. +- `openclaw ltm health` — Check PowerMem service health. - `openclaw ltm add ""` — Manually store one memory. --- @@ -321,7 +317,7 @@ Exposed to OpenClaw agents: **1. `openclaw ltm health` fails or cannot connect** - **CLI:** `pmem` on PATH or correct `pmemPath`; valid `.env` at `envFile`. -- **HTTP:** PowerMem server running; `baseUrl` matches (e.g. `http://localhost:8000`; avoid mixing `127.0.0.1` vs `localhost` unless intentional). +- **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`. **2. Add/search returns nothing or 500** @@ -337,7 +333,7 @@ Exposed to OpenClaw agents: **4. Agent does not search memory until I ask it to** - With `autoRecall: true`, the plugin injects system guidance so the agent is told to use `memory_recall` (or injected ``) when answering about past events, preferences, or people. Ensure `autoRecall` is not set to `false`. -- Auto-recall runs before each turn using the current user message (or the last user message if the prompt is very short). If you still see the agent answering without checking memory, try being explicit once (e.g. "check your memory for …") and ensure the run uses the plugin (e.g. web UI after `/new` uses the same gateway and plugin). +- Auto-recall runs before each turn using the current user message (or the previous user message if the prompt is very short). If the agent still replies without querying memory, try saying explicitly once “check memory for …” to confirm the pipeline; ensure the Web session after `/new` uses the same gateway and plugin. **5. Agent tries to read `memory/YYYY-MM-DD.md` and gets ENOENT** diff --git a/README_CN.md b/README_CN.md index edc3e68..32c8c80 100644 --- a/README_CN.md +++ b/README_CN.md @@ -26,7 +26,7 @@ ## 第一步:安装并启动 PowerMem -可选 **方式 C(CLI,推荐给 OpenClaw 个人用户)**、**方式 A(HTTP + pip)** 或 **方式 B(Docker)**。 +可选 **方式 A(CLI,推荐给 OpenClaw 个人用户)**、**方式 B(HTTP + pip)** 或 **方式 C(Docker)**。 ### 方式 C:CLI + SQLite(推荐给个人) @@ -48,7 +48,7 @@ --- -### 方式 A:用 pip 安装(本机跑 HTTP 服务) +### 方式 B:用 pip 安装(本机跑 HTTP 服务) 适合要**单独起 API 服务**、或不使用 CLI 模式的场景。适合本机已有 Python 3.11+ 的情况。 @@ -119,7 +119,7 @@ curl -s http://localhost:8000/api/v1/system/health --- -### 方式 B:用 Docker 运行(不装 Python 也行) +### 方式 C:用 Docker 运行(不装 Python 也行) 适合本机有 Docker、不想装 Python 的情况。 diff --git a/openclaw-powermem-env.ts b/openclaw-powermem-env.ts index 33c7c41..22f676a 100644 --- a/openclaw-powermem-env.ts +++ b/openclaw-powermem-env.ts @@ -59,11 +59,24 @@ function secretToString(v: unknown): string | undefined { return undefined; } +/** + * DashScope native HTTP API base (Generation / TextEmbedding SDK). + * OpenClaw "bailian" often uses OpenAI-compatible `/compatible-mode/v1`, which must not be used for PowerMem's qwen embedder. + */ +function dashscopeNativeBaseUrl(openclawBaseUrl: string | undefined): string | undefined { + if (!openclawBaseUrl?.trim()) return undefined; + const u = openclawBaseUrl.trim().replace(/\/+$/, ""); + if (u.includes("/compatible-mode/")) { + return u.replace(/\/compatible-mode\/v1$/i, "/api/v1"); + } + return u; +} + /** Map OpenClaw catalog provider id → PowerMem LLM provider name where they differ. */ function normalizePowermemProvider(openclawProvider: string): string { const p = openclawProvider.toLowerCase(); if (p === "google" || p === "google-generative-ai") return "gemini"; - if (p === "dashscope") return "qwen"; + if (p === "dashscope" || p === "bailian") return "qwen"; return p; } @@ -195,6 +208,10 @@ export async function buildPowermemCliProcessEnv( if (apiKey) out.EMBEDDING_API_KEY = apiKey; out.EMBEDDING_MODEL = "text-embedding-v4"; out.EMBEDDING_DIMS = "1536"; + { + const native = dashscopeNativeBaseUrl(baseUrl); + if (native) out.DASHSCOPE_BASE_URL = native; + } break; } case "ollama": { diff --git a/test/openclaw-powermem-env.test.ts b/test/openclaw-powermem-env.test.ts index 94c6452..b92c0be 100644 --- a/test/openclaw-powermem-env.test.ts +++ b/test/openclaw-powermem-env.test.ts @@ -67,4 +67,25 @@ describe("buildPowermemCliProcessEnv", () => { expect(env.LLM_MODEL).toBe("qwen-plus"); expect(env.EMBEDDING_PROVIDER).toBe("qwen"); }); + + it("maps bailian to qwen embedding (text-embedding-v4) and native DashScope base URL", async () => { + const env = await buildPowermemCliProcessEnv({ + openclawConfig: { + agents: { defaults: { model: "bailian/qwen-plus" } }, + models: { + providers: { + bailian: { + baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + apiKey: "bk", + }, + }, + }, + }, + stateDir: "/tmp/memory-powermem-test-state", + resolveProviderAuth: async () => ({}), + }); + expect(env.LLM_PROVIDER).toBe("qwen"); + expect(env.EMBEDDING_MODEL).toBe("text-embedding-v4"); + expect(env.DASHSCOPE_BASE_URL).toBe("https://dashscope.aliyuncs.com/api/v1"); + }); });