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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway
| `autoExperience` | No | Auto-extract procedural experiences via LLM; default `true`. |
| `experienceRecall` | No | Include experiences in recall results; default `true`. |
| `inferOnAdd` | No | Use PowerMem intelligent extraction when adding; default `true`. |
| `importMarkdownOnStart` | No | One-time import of existing OpenClaw markdown memories on startup; default `false`. |
| `importMarkdownPaths` | No | Markdown files/directories to import. Defaults to `memory/`, `MEMORY.md`, and `USER.md`; relative paths resolve from the OpenClaw workspace. |
| `importMarkdownMaxFileBytes` | No | Max size for a single markdown file; default `10485760` (10 MiB). Larger files are marked `skipped_too_large`. |
| `importMarkdownBatchDelayMs` | No | Delay between imported chunks to avoid write bursts; default `300`. |
| `importMarkdownMaxFiles` | No | Optional hard cap on markdown files imported in one run. Empty means no cap. |
| `importMarkdownMaxChunks` | No | Optional hard cap on markdown chunks imported in one run. Empty means no cap. |
| `dualWrite` | No | HTTP only: write to remote + local SQLite and queue failed writes. |
| `dualWritePriority` | No | Dual-write priority: `"remote"` (default) tries PowerMem first and falls back to local SQLite; `"local"` writes/searches SQLite first and syncs to remote. |
| `localDbPath` | No | Local SQLite path for `dualWrite`. |
Expand Down Expand Up @@ -341,6 +347,8 @@ Exposed to OpenClaw agents:
- `openclaw ltm search <query> [--limit n]` — Search memories.
- `openclaw ltm health` — Check PowerMem service health.
- `openclaw ltm add "<text>"` — Manually store one memory.
- `openclaw ltm import-md [paths...] [--force] [--dry-run] [--delay-ms n] [--max-file-bytes n] [--max-files n] [--max-chunks n]` — Import existing markdown memories. With no paths, scans `memory/`, `MEMORY.md`, and `USER.md`.
- `openclaw ltm import-md-status [paths...] [--json]` — Show per-file markdown import status: imported, changed, skipped, failed, or not imported.

---

Expand Down
8 changes: 8 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ openclaw ltm search "咖啡"
| `autoExperience` | 否 | LLM 自动提炼经验,默认 `true`。 |
| `experienceRecall` | 否 | 召回结果是否包含经验,默认 `true`。 |
| `inferOnAdd` | 否 | 写入时是否用 PowerMem 智能抽取,默认 `true`。 |
| `importMarkdownOnStart` | 否 | 启动时一次性导入已有 OpenClaw markdown 记忆,默认 `false`。 |
| `importMarkdownPaths` | 否 | 要导入的 markdown 文件或目录。默认扫描 `memory/`、`MEMORY.md`、`USER.md`;相对路径基于 OpenClaw workspace。 |
| `importMarkdownMaxFileBytes` | 否 | 单个 markdown 文件最大大小,默认 `10485760`(10 MiB);超出的文件标记为 `skipped_too_large`。 |
| `importMarkdownBatchDelayMs` | 否 | 每个导入 chunk 之间的延迟,用于避免写入洪峰;默认 `300`。 |
| `importMarkdownMaxFiles` | 否 | 单次导入的 markdown 文件硬上限;不填表示不限制。 |
| `importMarkdownMaxChunks` | 否 | 单次导入的 markdown chunk 硬上限;不填表示不限制。 |
| `dualWrite` | 否 | 仅 HTTP:远端 + 本地 SQLite 双写,远端失败自动排队补传。 |
| `dualWritePriority` | 否 | 双写优先级:`"remote"`(默认)先远端 PowerMem、失败兜底本地 SQLite;`"local"` 先写/查 SQLite,再同步到远端。 |
| `localDbPath` | 否 | 本地 SQLite 路径(`dualWrite`)。 |
Expand Down Expand Up @@ -342,6 +348,8 @@ openclaw ltm search "咖啡"
- `openclaw ltm search <query> [--limit n]` — 搜索记忆
- `openclaw ltm health` — 检查 PowerMem 服务健康
- `openclaw ltm add "<text>"` — 手动写入一条记忆
- `openclaw ltm import-md [paths...] [--force] [--dry-run] [--delay-ms n] [--max-file-bytes n] [--max-files n] [--max-chunks n]` — 导入已有 markdown 记忆;不传路径时扫描 `memory/`、`MEMORY.md`、`USER.md`
- `openclaw ltm import-md-status [paths...] [--json]` — 查看每个 markdown 文件的导入状态:已导入、已变更、跳过、失败或未导入

---

Expand Down
18 changes: 16 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,19 @@ deploy_from_repo() {
if [[ -f "README.md" ]]; then
cp README.md "${PLUGIN_DEST}/" || true
fi
if [[ -d "scripts" ]]; then
rm -rf "${PLUGIN_DEST}/scripts"
cp -R scripts "${PLUGIN_DEST}/"
fi
info "Installing plugin dependencies..."
(cd "${PLUGIN_DEST}" && npm install --no-audit --no-fund) || {
(cd "${PLUGIN_DEST}" && npm install --no-audit --no-fund --ignore-scripts=false) || {
err "npm install failed in ${PLUGIN_DEST}"
exit 1
}
(cd "${PLUGIN_DEST}" && npm run native:verify) || {
err "native dependency verification failed in ${PLUGIN_DEST}"
exit 1
}
info "Plugin deployed: ${PLUGIN_DEST}"
}

Expand All @@ -243,13 +251,15 @@ deploy_from_github() {
"openclaw-powermem-env.ts"
"openclaw.plugin.json"
"package.json"
"scripts/ensure-native-deps.cjs"
"tsconfig.json"
".gitignore"
)
mkdir -p "${PLUGIN_DEST}"
info "Downloading plugin from ${REPO}@${BRANCH}..."
for f in "${files[@]}"; do
local url="${gh_raw}/${f}"
mkdir -p "$(dirname "${PLUGIN_DEST}/${f}")"
if curl -fsSL --connect-timeout 15 --max-time 60 -o "${PLUGIN_DEST}/${f}" "${url}" 2>/dev/null; then
echo " ${f} ✓"
else
Expand All @@ -260,10 +270,14 @@ deploy_from_github() {
fi
done
info "Installing plugin dependencies..."
(cd "${PLUGIN_DEST}" && npm install --no-audit --no-fund) || {
(cd "${PLUGIN_DEST}" && npm install --no-audit --no-fund --ignore-scripts=false) || {
err "npm install failed in ${PLUGIN_DEST}"
exit 1
}
(cd "${PLUGIN_DEST}" && npm run native:verify) || {
err "native dependency verification failed in ${PLUGIN_DEST}"
exit 1
}
info "Plugin deployed: ${PLUGIN_DEST}"
}

Expand Down
44 changes: 44 additions & 0 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,41 @@
"label": "Infer on Add",
"help": "Use PowerMem intelligent extraction when adding (infer=true)"
},
"importMarkdownOnStart": {
"label": "Import markdown on start",
"advanced": true,
"help": "One-time import of existing OpenClaw markdown memories on service start. Defaults to memory/, MEMORY.md, and USER.md."
},
"importMarkdownPaths": {
"label": "Markdown import paths",
"advanced": true,
"placeholder": "[\"memory\", \"MEMORY.md\", \"USER.md\"]",
"help": "Files or directories to import when importMarkdownOnStart is enabled. Relative paths resolve from the OpenClaw workspace."
},
"importMarkdownMaxFileBytes": {
"label": "Markdown import max file bytes",
"advanced": true,
"placeholder": "10485760",
"help": "Max size for a single markdown file. Files above this are marked skipped_too_large. Default: 10 MiB."
},
"importMarkdownBatchDelayMs": {
"label": "Markdown import delay (ms)",
"advanced": true,
"placeholder": "300",
"help": "Delay between imported chunks to avoid startup write bursts. Default: 300ms."
},
"importMarkdownMaxFiles": {
"label": "Markdown import max files",
"advanced": true,
"placeholder": "",
"help": "Optional hard cap on markdown files imported in one run. Empty means no cap."
},
"importMarkdownMaxChunks": {
"label": "Markdown import max chunks",
"advanced": true,
"placeholder": "",
"help": "Optional hard cap on markdown chunks imported in one run. Empty means no cap."
},
"debugPerfLog": {
"label": "Debug performance logs",
"advanced": true,
Expand Down Expand Up @@ -204,6 +239,15 @@
"autoExperience": { "type": "boolean" },
"experienceRecall": { "type": "boolean" },
"inferOnAdd": { "type": "boolean" },
"importMarkdownOnStart": { "type": "boolean" },
"importMarkdownPaths": {
"type": "array",
"items": { "type": "string" }
},
"importMarkdownMaxFileBytes": { "type": "number" },
"importMarkdownBatchDelayMs": { "type": "number" },
"importMarkdownMaxFiles": { "type": "number" },
"importMarkdownMaxChunks": { "type": "number" },
"debugPerfLog": { "type": "boolean" },
"perfSlowMs": { "type": "number" },
"dualWrite": { "type": "boolean" },
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"files": [
"src",
"lib",
"scripts",
"openclaw.plugin.json"
],
"scripts": {
"postinstall": "node scripts/ensure-native-deps.cjs",
"native:verify": "node scripts/ensure-native-deps.cjs",
"test": "vitest run",
"lint": "tsc --noEmit"
},
Expand Down Expand Up @@ -46,7 +49,8 @@
"license": "Apache-2.0",
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3"
"better-sqlite3",
"sqlite-vec"
]
}
}
100 changes: 100 additions & 0 deletions scripts/ensure-native-deps.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env node
"use strict";

const { spawnSync } = require("node:child_process");
const path = require("node:path");

const rootDir = path.resolve(__dirname, "..");
const nativePackages = ["better-sqlite3", "sqlite-vec"];

function log(message) {
console.log(`[memory-powermem] ${message}`);
}

function warn(message) {
console.warn(`[memory-powermem] ${message}`);
}

function npmCommand() {
return process.platform === "win32" ? "npm.cmd" : "npm";
}

function tryRequire(packageName) {
try {
const resolved = require.resolve(packageName, { paths: [rootDir] });
require(resolved);
return null;
} catch (err) {
return err;
}
}

function verifyNativePackages() {
const failures = [];
for (const packageName of nativePackages) {
const err = tryRequire(packageName);
if (err) {
failures.push({ packageName, err });
}
}
return failures;
}

function printFailures(failures) {
for (const { packageName, err } of failures) {
warn(`${packageName} failed to load: ${err && err.message ? err.message : String(err)}`);
}
}

function rebuildNativePackages() {
log(`rebuilding native dependencies: ${nativePackages.join(", ")}`);
return spawnSync(
npmCommand(),
["rebuild", ...nativePackages, "--build-from-source"],
{
cwd: rootDir,
env: {
...process.env,
npm_config_ignore_scripts: "false",
},
stdio: "inherit",
},
);
}

function main() {
if (process.env.MEMORY_POWERMEM_SKIP_NATIVE_REBUILD === "1") {
warn("skipping native dependency verification because MEMORY_POWERMEM_SKIP_NATIVE_REBUILD=1");
return;
}

const initialFailures = verifyNativePackages();
if (initialFailures.length === 0) {
log("native dependencies verified");
return;
}

printFailures(initialFailures);
const result = rebuildNativePackages();
if (result.error) {
warn(`failed to run npm rebuild: ${result.error.message}`);
process.exit(1);
}
if (result.status !== 0) {
warn("native dependency rebuild failed");
warn("install build tools first, then reinstall or run: npm rebuild better-sqlite3 sqlite-vec --build-from-source");
warn("Debian/Ubuntu example: apt-get update && apt-get install -y python3 make gcc g++");
process.exit(result.status ?? 1);
}

const finalFailures = verifyNativePackages();
if (finalFailures.length > 0) {
printFailures(finalFailures);
warn("native dependencies still failed after rebuild");
process.exit(1);
}

log("native dependencies rebuilt and verified");
}

main();
8 changes: 8 additions & 0 deletions skills/install-memory-powermem-full/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Quick reference for skill **`install-memory-powermem-full`**. See **SKILL.md** i
| `autoExperience` | `true` | Auto-extract experiences via LLM. |
| `experienceRecall` | `true` | Include experiences in recall. |
| `inferOnAdd` | `true` | PowerMem intelligent extraction on add. |
| `importMarkdownOnStart` | `false` | One-time import of existing OpenClaw markdown memories on startup. |
| `importMarkdownPaths` | `memory`, `MEMORY.md`, `USER.md` | Markdown files/directories to import; relative paths resolve from the OpenClaw workspace. |
| `importMarkdownMaxFileBytes` | `10485760` | Max size for a single markdown file (10 MiB); larger files are marked `skipped_too_large`. |
| `importMarkdownBatchDelayMs` | `300` | Delay between imported chunks to avoid write bursts. |
| `importMarkdownMaxFiles` | — | Optional hard cap on markdown files imported in one run. |
| `importMarkdownMaxChunks` | — | Optional hard cap on markdown chunks imported in one run. |
| `userId` | auto | Omit or set to `auto` to generate a stable ID saved under `<stateDir>/powermem/identity.json`. |
| `agentId` | auto | Omit or set to `auto` to generate a stable ID saved under `<stateDir>/powermem/identity.json`. |
| `dualWrite` | `false` | HTTP only: remote + local SQLite dual-write. |
Expand Down Expand Up @@ -96,6 +102,8 @@ Quick reference for skill **`install-memory-powermem-full`**. See **SKILL.md** i
openclaw ltm health
openclaw ltm add "Something to remember"
openclaw ltm search "query"
openclaw ltm import-md [paths...] [--force] [--dry-run] [--delay-ms n] [--max-file-bytes n] [--max-files n] [--max-chunks n]
openclaw ltm import-md-status [paths...] [--json]

openclaw config set plugins.slots.memory none
openclaw config set plugins.slots.memory memory-powermem
Expand Down
29 changes: 29 additions & 0 deletions src/better-sqlite3.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
declare module "better-sqlite3" {
namespace Database {
interface RunResult {
changes: number;
lastInsertRowid: number | bigint;
}

interface Statement {
run(...params: unknown[]): RunResult;
get(...params: unknown[]): any;
all(...params: unknown[]): any[];
}

interface Database {
exec(sql: string): this;
prepare(sql: string): Statement;
loadExtension(path: string): void;
close(): void;
}
}

interface DatabaseConstructor {
new (filename: string, options?: Record<string, unknown>): Database.Database;
(filename: string, options?: Record<string, unknown>): Database.Database;
}

const Database: DatabaseConstructor;
export = Database;
}
Loading
Loading