From e592ff31a7bbfe96bd1c33b2aff8c7fb239d717d Mon Sep 17 00:00:00 2001 From: Teingi Date: Thu, 19 Mar 2026 19:50:19 +0800 Subject: [PATCH 1/2] 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/2] 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"); + }); +});