clawmem 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -742,6 +742,7 @@ clawmem focus clear --session-id abc123
742
742
  - OpenClaw memory plugin: `clawmem setup openclaw` — registers ClawMem as a native OpenClaw memory plugin (`kind: memory`, v0.10.0+). Lifecycle events on the plugin-hook bus: `before_prompt_build` is the **load-bearing** path — it runs prompt-aware retrieval (context-surfacing) AND the pre-emptive `precompact-extract` synchronously when token usage approaches the compaction threshold, so state is captured BEFORE the LLM call that could trigger compaction; `agent_end` runs decision-extractor + handoff-generator + feedback-loop in parallel (fire-and-forget at OpenClaw, plus a 30s default void-hook timeout from OpenClaw v2026.4.26+ that logs slow handlers but does not cancel the underlying postrun work); `before_compaction` is **defense-in-depth fallback only** — fire-and-forget, races the compactor, exists for the rare case where the `before_prompt_build` proximity heuristic missed a sudden token jump; `session_start` registers the session and caches first-turn bootstrap context. Shares the same vault as Claude Code hooks (dual-mode). SQLite busy_timeout=5000ms for concurrent access safety.
743
743
  - **§14.3 pure-memory migration (v0.10.0):** v0.10.0 drops the `ClawMemContextEngine` class entirely. Previous versions registered as `kind: context-engine` and implemented `assemble()`/`bootstrap()`/`afterTurn()`/`compact()` on a class. v0.10.0 registers as `kind: memory` and wires every lifecycle surface through plugin hooks on the event bus. Retrieval pipeline, composite scoring, vault format, and the 5 registered agent tools are unchanged — this is a packaging and registration change, not a behavioral one.
744
744
  - **v2026.4.11 packaging fix (v0.10.0):** `src/openclaw/package.json` declares `openclaw.extensions: ["./index.ts"]` (required by v2026.4.11's discovery path), and `cmdSetupOpenClaw` defaults to `cpSync(..., { recursive: true, dereference: true })` because v2026.4.11's discoverer uses `readdirSync({ withFileTypes: true })` where symlink `isDirectory() === false`. A `--link` opt-in flag preserves the old symlink behavior for dev workflows with a warning.
745
+ - **Profile-aware install (v0.10.4, §28.1, issue #11):** `cmdSetupOpenClaw` is now a three-path installer. When `openclaw` is on `PATH` it delegates to `openclaw plugins install <pluginDir> --force` (or `-l` for `--link`), inheriting OpenClaw's destination resolution (`OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, `--profile`), manifest validation, security scan, install records, slot selection, and registry refresh. The plugin is auto-enabled — Path 1's "Next steps" output drops `openclaw plugins enable clawmem` because it's redundant. `--link` in delegated mode records the source in `plugins.load.paths` (NOT a filesystem symlink, so the v2026.4.11 discovery skip does NOT apply). When `openclaw` is absent, ClawMem falls back to recursive copy at a destination resolved by `src/openclaw-paths.ts:resolveExtensionsDirNoOpenClaw` — a faithful mirror of OpenClaw's `resolveConfigDir` (priority: `OPENCLAW_STATE_DIR` → `OPENCLAW_CONFIG_PATH` → `OPENCLAW_HOME`/`HOME`/`USERPROFILE`/`os.homedir()`/`cwd` → `~/.openclaw`). The fallback's filesystem symlink in `--link` mode is still subject to OpenClaw's discovery skip (warning surfaced). `--remove` tries `openclaw plugins uninstall clawmem --force` first; on failure (typical for legacy unmanaged direct-copy installs), warns the user that config/install records may need manual repair, then runs manual cleanup at the resolved path. On success, also performs a constrained stale cleanup of any leftover unmanaged directory at the same path. `--help` / `-h` short-circuits before any spawn or filesystem work and prints the full flag + env-var reference (§28.2). Helpers in `src/openclaw-paths.ts` take injected `env` + `homedir` for testability.
745
746
  - **v2026.4.18 synchronous-`register()` constraint:** OpenClaw v2026.4.18 (`fix(plugins): enforce synchronous registration`) throws `"plugin register must be synchronous"` if the plugin's `register()` function returns a Promise. ClawMem's `register(api)` in `src/openclaw/index.ts` is intentionally synchronous — all `await` work lives inside per-event handlers, never in registration itself. Companion change: register failures now atomically roll back side effects (globals, hook registrations, tool registrations), so any future throw inside `register()` will leave OpenClaw in a clean state. Keep the function synchronous and throw-free; do not add `async` or top-level `await`.
746
747
  - **Precompact correctness contract (v0.10.0):** The load-bearing precompact path is `before_prompt_build`, NOT `before_compaction`. `before_prompt_build` is awaited synchronously before the LLM call that could trigger compaction, so it cannot race the compactor. `before_compaction` is fire-and-forget at OpenClaw's call site and exists only as a safety net for the rare case the proximity heuristic in `before_prompt_build` missed a sudden token-count jump. Do not describe `before_compaction` as the primary surface — the guarantee comes from `before_prompt_build`. v0.3.0 did the pre-emptive extraction from `ContextEngine.compact()` via `delegateCompactionToRuntime()`; v0.10.0 moves it into `before_prompt_build` where it can be awaited before the triggering LLM call. User-visible behavior is equivalent or better: state capture now happens strictly before compaction, not in a race with it.
747
748
  - Hermes Agent MemoryProvider plugin: `src/hermes/` — Python plugin implementing Hermes's `MemoryProvider` ABC. **Preferred install:** copy into `$HERMES_HOME/plugins/clawmem/` (typically `~/.hermes/plugins/clawmem/`) — Hermes #10529 (v2026.4.13+) added user-plugin discovery, so this path survives `git pull` of hermes-agent. **Bundled-style install:** `hermes-agent/plugins/memory/clawmem/` still works (bundled-first precedence on name collisions). Uses shell-out for lifecycle hooks (session-bootstrap, context-surfacing, extraction) and REST API for tools (retrieve, get, session_log, timeline, similar). Plugin manages its own transcript JSONL for ClawMem hooks. Supports external (you run `clawmem serve`) and managed (plugin starts/stops serve) modes. **Agent-context isolation:** `initialize()` reads the `agent_context` kwarg Hermes passes ("primary"/"subagent"/"cron"/"flush"); for non-primary contexts the read-side hooks (session-bootstrap, context-surfacing) still run but the write-side surfaces (`sync_turn` transcript appends, `on_session_end` extraction, `on_pre_compress` precompact) early-return so cron system prompts and subagent intermediate state never reach the vault.
package/CLAUDE.md CHANGED
@@ -742,6 +742,7 @@ clawmem focus clear --session-id abc123
742
742
  - OpenClaw memory plugin: `clawmem setup openclaw` — registers ClawMem as a native OpenClaw memory plugin (`kind: memory`, v0.10.0+). Lifecycle events on the plugin-hook bus: `before_prompt_build` is the **load-bearing** path — it runs prompt-aware retrieval (context-surfacing) AND the pre-emptive `precompact-extract` synchronously when token usage approaches the compaction threshold, so state is captured BEFORE the LLM call that could trigger compaction; `agent_end` runs decision-extractor + handoff-generator + feedback-loop in parallel (fire-and-forget at OpenClaw, plus a 30s default void-hook timeout from OpenClaw v2026.4.26+ that logs slow handlers but does not cancel the underlying postrun work); `before_compaction` is **defense-in-depth fallback only** — fire-and-forget, races the compactor, exists for the rare case where the `before_prompt_build` proximity heuristic missed a sudden token jump; `session_start` registers the session and caches first-turn bootstrap context. Shares the same vault as Claude Code hooks (dual-mode). SQLite busy_timeout=5000ms for concurrent access safety.
743
743
  - **§14.3 pure-memory migration (v0.10.0):** v0.10.0 drops the `ClawMemContextEngine` class entirely. Previous versions registered as `kind: context-engine` and implemented `assemble()`/`bootstrap()`/`afterTurn()`/`compact()` on a class. v0.10.0 registers as `kind: memory` and wires every lifecycle surface through plugin hooks on the event bus. Retrieval pipeline, composite scoring, vault format, and the 5 registered agent tools are unchanged — this is a packaging and registration change, not a behavioral one.
744
744
  - **v2026.4.11 packaging fix (v0.10.0):** `src/openclaw/package.json` declares `openclaw.extensions: ["./index.ts"]` (required by v2026.4.11's discovery path), and `cmdSetupOpenClaw` defaults to `cpSync(..., { recursive: true, dereference: true })` because v2026.4.11's discoverer uses `readdirSync({ withFileTypes: true })` where symlink `isDirectory() === false`. A `--link` opt-in flag preserves the old symlink behavior for dev workflows with a warning.
745
+ - **Profile-aware install (v0.10.4, §28.1, issue #11):** `cmdSetupOpenClaw` is now a three-path installer. When `openclaw` is on `PATH` it delegates to `openclaw plugins install <pluginDir> --force` (or `-l` for `--link`), inheriting OpenClaw's destination resolution (`OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, `--profile`), manifest validation, security scan, install records, slot selection, and registry refresh. The plugin is auto-enabled — Path 1's "Next steps" output drops `openclaw plugins enable clawmem` because it's redundant. `--link` in delegated mode records the source in `plugins.load.paths` (NOT a filesystem symlink, so the v2026.4.11 discovery skip does NOT apply). When `openclaw` is absent, ClawMem falls back to recursive copy at a destination resolved by `src/openclaw-paths.ts:resolveExtensionsDirNoOpenClaw` — a faithful mirror of OpenClaw's `resolveConfigDir` (priority: `OPENCLAW_STATE_DIR` → `OPENCLAW_CONFIG_PATH` → `OPENCLAW_HOME`/`HOME`/`USERPROFILE`/`os.homedir()`/`cwd` → `~/.openclaw`). The fallback's filesystem symlink in `--link` mode is still subject to OpenClaw's discovery skip (warning surfaced). `--remove` tries `openclaw plugins uninstall clawmem --force` first; on failure (typical for legacy unmanaged direct-copy installs), warns the user that config/install records may need manual repair, then runs manual cleanup at the resolved path. On success, also performs a constrained stale cleanup of any leftover unmanaged directory at the same path. `--help` / `-h` short-circuits before any spawn or filesystem work and prints the full flag + env-var reference (§28.2). Helpers in `src/openclaw-paths.ts` take injected `env` + `homedir` for testability.
745
746
  - **v2026.4.18 synchronous-`register()` constraint:** OpenClaw v2026.4.18 (`fix(plugins): enforce synchronous registration`) throws `"plugin register must be synchronous"` if the plugin's `register()` function returns a Promise. ClawMem's `register(api)` in `src/openclaw/index.ts` is intentionally synchronous — all `await` work lives inside per-event handlers, never in registration itself. Companion change: register failures now atomically roll back side effects (globals, hook registrations, tool registrations), so any future throw inside `register()` will leave OpenClaw in a clean state. Keep the function synchronous and throw-free; do not add `async` or top-level `await`.
746
747
  - **Precompact correctness contract (v0.10.0):** The load-bearing precompact path is `before_prompt_build`, NOT `before_compaction`. `before_prompt_build` is awaited synchronously before the LLM call that could trigger compaction, so it cannot race the compactor. `before_compaction` is fire-and-forget at OpenClaw's call site and exists only as a safety net for the rare case the proximity heuristic in `before_prompt_build` missed a sudden token-count jump. Do not describe `before_compaction` as the primary surface — the guarantee comes from `before_prompt_build`. v0.3.0 did the pre-emptive extraction from `ContextEngine.compact()` via `delegateCompactionToRuntime()`; v0.10.0 moves it into `before_prompt_build` where it can be awaited before the triggering LLM call. User-visible behavior is equivalent or better: state capture now happens strictly before compaction, not in a race with it.
747
748
  - Hermes Agent MemoryProvider plugin: `src/hermes/` — Python plugin implementing Hermes's `MemoryProvider` ABC. **Preferred install:** copy into `$HERMES_HOME/plugins/clawmem/` (typically `~/.hermes/plugins/clawmem/`) — Hermes #10529 (v2026.4.13+) added user-plugin discovery, so this path survives `git pull` of hermes-agent. **Bundled-style install:** `hermes-agent/plugins/memory/clawmem/` still works (bundled-first precedence on name collisions). Uses shell-out for lifecycle hooks (session-bootstrap, context-surfacing, extraction) and REST API for tools (retrieve, get, session_log, timeline, similar). Plugin manages its own transcript JSONL for ClawMem hooks. Supports external (you run `clawmem serve`) and managed (plugin starts/stops serve) modes. **Agent-context isolation:** `initialize()` reads the `agent_context` kwarg Hermes passes ("primary"/"subagent"/"cron"/"flush"); for non-primary contexts the read-side hooks (session-bootstrap, context-surfacing) still run but the write-side surfaces (`sync_turn` transcript appends, `on_session_end` extraction, `on_pre_compress` precompact) early-return so cron system prompts and subagent intermediate state never reach the vault.
package/README.md CHANGED
@@ -188,7 +188,13 @@ clawmem setup mcp # Register MCP server in ~/.claude.json (31 tools)
188
188
  ClawMem registers as a native OpenClaw memory plugin (`kind: memory`, v0.10.0+). Same 90/10 automatic retrieval, delivered through OpenClaw's plugin-hook bus instead of Claude Code hooks.
189
189
 
190
190
  ```bash
191
- clawmem setup openclaw # Installs plugin into ~/.openclaw/extensions/clawmem (copy, not symlink)
191
+ # v0.10.4+: profile-aware. Delegates to `openclaw plugins install --force` when the OpenClaw CLI
192
+ # is on PATH (auto-enables the plugin, honors OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, --profile).
193
+ # Falls back to a recursive copy honoring OPENCLAW_STATE_DIR when the CLI is absent.
194
+ clawmem setup openclaw
195
+
196
+ # Custom profile (e.g. dev profile at ~/.openclaw-dev):
197
+ OPENCLAW_STATE_DIR=~/.openclaw-dev clawmem setup openclaw
192
198
  ```
193
199
 
194
200
  **What the plugin provides:**
@@ -1099,13 +1105,16 @@ clawmem install-service --enable
1099
1105
  #### OpenClaw-specific
1100
1106
 
1101
1107
  ```bash
1102
- # Install the OpenClaw plugin (v0.10.0+: recursively copies into ~/.openclaw/extensions/clawmem)
1108
+ # Install the OpenClaw plugin
1109
+ # v0.10.4+: delegates to `openclaw plugins install --force` when the CLI is on PATH (auto-enables;
1110
+ # honors OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH / --profile). Falls back to recursive copy
1111
+ # honoring OPENCLAW_STATE_DIR when the CLI is absent.
1112
+ # v0.10.0–v0.10.3: hardcoded recursive copy into ~/.openclaw/extensions/clawmem (issue #11).
1103
1113
  clawmem setup openclaw
1104
- # Then follow the printed next steps:
1105
- # 1. openclaw plugins enable clawmem (switches memory slot + disables memory-core)
1106
- # 2. openclaw gateway restart
1107
- # 3. Configure GPU endpoints if needed (see setup openclaw output)
1108
- # Multi-user installs also need: sudo chown -R <gateway-user>:<gateway-group> ~/.openclaw/extensions/clawmem
1114
+ # Then follow the printed next steps. The exact list depends on which path ran:
1115
+ # Path 1 (delegated): plugin already auto-enabled just `openclaw gateway restart` + GPU config
1116
+ # Path 3 (CLI absent): manually `openclaw plugins enable clawmem`, then restart + GPU config
1117
+ # Multi-user installs also need: sudo chown -R <gateway-user>:<gateway-group> <extensions>/clawmem
1109
1118
  # Requires OpenClaw v2026.4.11+.
1110
1119
  ```
1111
1120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmem",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "On-device memory layer for AI agents. Claude Code, OpenClaw, and Hermes. Hooks + MCP server + hybrid RAG search.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/clawmem.ts CHANGED
@@ -70,6 +70,10 @@ import {
70
70
  clearSessionFocus,
71
71
  focusFilePath,
72
72
  } from "./session-focus.ts";
73
+ import {
74
+ resolveExtensionsDirNoOpenClaw,
75
+ printSetupOpenClawHelp,
76
+ } from "./openclaw-paths.ts";
73
77
 
74
78
  enableProductionMode();
75
79
 
@@ -1324,13 +1328,26 @@ function readOpenClawConfigValue(key: string): string | undefined {
1324
1328
  }
1325
1329
 
1326
1330
  async function cmdSetupOpenClaw(args: string[]) {
1331
+ // §28.2 — short-circuit on --help / -h before any spawn or filesystem work.
1332
+ if (args.includes("--help") || args.includes("-h")) {
1333
+ printSetupOpenClawHelp();
1334
+ return;
1335
+ }
1336
+
1327
1337
  const remove = args.includes("--remove");
1328
1338
  const linkMode = args.includes("--link");
1329
1339
  const pluginDir = pathResolve(import.meta.dir, "openclaw");
1330
- const extensionsDir = pathResolve(process.env.HOME || "~", ".openclaw", "extensions");
1340
+
1341
+ // Resolve the extensions/clawmem path we would touch directly. Both Path 1
1342
+ // link-mode pre-cleanup and Path 3 direct-copy install need this. Path 1
1343
+ // copy-mode delegation does NOT use linkPath because OpenClaw's
1344
+ // `--force` install owns the destination resolution there.
1345
+ const extensionsDir = resolveExtensionsDirNoOpenClaw();
1331
1346
  const linkPath = pathResolve(extensionsDir, "clawmem");
1332
1347
 
1333
- // Check if openclaw CLI is available
1348
+ // Probe whether the openclaw CLI is on PATH. Used to choose between
1349
+ // delegation (Path 1) and direct-copy fallback (Path 3) for installs,
1350
+ // and between CLI uninstall and manual cleanup for --remove.
1334
1351
  const hasOpenClawCli = (() => {
1335
1352
  try {
1336
1353
  const r = Bun.spawnSync(["openclaw", "--version"], { stdout: "pipe", stderr: "pipe" });
@@ -1339,27 +1356,65 @@ async function cmdSetupOpenClaw(args: string[]) {
1339
1356
  })();
1340
1357
 
1341
1358
  if (remove) {
1342
- // Actually uninstall mirror of install behavior
1359
+ // §28.1 H3 / R1 / R4: try-and-fall-back uninstall + constrained stale
1360
+ // cleanup. CLI uninstall is preferred (handles managed config + slot
1361
+ // resets); manual cleanup is the legacy fallback for unmanaged
1362
+ // direct-cpSync installs from older ClawMem versions. On CLI failure we
1363
+ // fall through AND emit a warning so the user knows config/install
1364
+ // records may need manual repair.
1365
+ let cliUninstallSucceeded = false;
1366
+ let cliUninstallFailed = false;
1367
+ if (hasOpenClawCli) {
1368
+ const r = Bun.spawnSync(
1369
+ ["openclaw", "plugins", "uninstall", "clawmem", "--force"],
1370
+ { stdout: "inherit", stderr: "inherit" },
1371
+ );
1372
+ if (r.exitCode === 0) {
1373
+ cliUninstallSucceeded = true;
1374
+ } else {
1375
+ cliUninstallFailed = true;
1376
+ console.log(
1377
+ `${c.yellow}Warning: openclaw plugins uninstall clawmem failed (exit ${r.exitCode}).${c.reset}`,
1378
+ );
1379
+ console.log(
1380
+ `${c.yellow} OpenClaw config and install records may still reference clawmem.${c.reset}`,
1381
+ );
1382
+ console.log(
1383
+ `${c.yellow} Falling back to manual cleanup of the install directory.${c.reset}`,
1384
+ );
1385
+ }
1386
+ }
1387
+
1388
+ // Constrained stale cleanup (R3 in BACKLOG §28.1): even if CLI uninstall
1389
+ // succeeded, an old unmanaged direct-copy directory at the same path
1390
+ // could still be present (managed-link + unmanaged-copy side-by-side).
1391
+ // Always check the exact extensions/clawmem path and remove if present.
1343
1392
  let removed = false;
1344
1393
  try {
1345
- const stat = await import("fs").then(m => m.lstatSync(linkPath));
1346
- if (stat.isSymbolicLink() || stat.isDirectory()) {
1347
- const { unlinkSync, rmSync } = await import("fs");
1348
- if (stat.isSymbolicLink()) {
1349
- unlinkSync(linkPath);
1350
- } else {
1351
- rmSync(linkPath, { recursive: true });
1352
- }
1353
- console.log(`${c.green}Removed plugin from ${linkPath}${c.reset}`);
1394
+ const { lstatSync, unlinkSync, rmSync } = await import("fs");
1395
+ const stat = lstatSync(linkPath);
1396
+ if (stat.isSymbolicLink()) {
1397
+ unlinkSync(linkPath);
1398
+ console.log(`${c.green}Removed plugin symlink at ${linkPath}${c.reset}`);
1399
+ removed = true;
1400
+ } else if (stat.isDirectory()) {
1401
+ rmSync(linkPath, { recursive: true });
1402
+ console.log(`${c.green}Removed plugin directory at ${linkPath}${c.reset}`);
1354
1403
  removed = true;
1355
1404
  }
1356
1405
  } catch (e: any) {
1357
1406
  if (e.code !== "ENOENT") throw e;
1358
- console.log(`${c.dim}Plugin not installed at ${linkPath}${c.reset}`);
1407
+ if (!cliUninstallSucceeded && !cliUninstallFailed) {
1408
+ // Truly nothing to do — no CLI, no directory.
1409
+ console.log(`${c.dim}Plugin not installed at ${linkPath}${c.reset}`);
1410
+ }
1359
1411
  }
1360
1412
 
1413
+ // Slot reset: only meaningful if the CLI is reachable. CLI uninstall
1414
+ // already clears the memory slot if it succeeded, but if uninstall
1415
+ // failed we still attempt slot reset because config slots can be
1416
+ // populated separately from install records.
1361
1417
  if (hasOpenClawCli) {
1362
- // Reset the memory slot if ClawMem owned it (post-§14.3-migration installs).
1363
1418
  const memSlot = readOpenClawConfigValue("plugins.slots.memory");
1364
1419
  if (memSlot === "clawmem") {
1365
1420
  Bun.spawnSync(["openclaw", "config", "unset", "plugins.slots.memory"], { stdout: "inherit", stderr: "inherit" });
@@ -1378,7 +1433,8 @@ async function cmdSetupOpenClaw(args: string[]) {
1378
1433
  return;
1379
1434
  }
1380
1435
 
1381
- // Verify plugin source files exist
1436
+ // Verify plugin source files exist (cheap defense-in-depth — surfaces
1437
+ // ClawMem packaging bugs immediately, before any spawn).
1382
1438
  if (!existsSync(pathResolve(pluginDir, "index.ts"))) {
1383
1439
  die(`OpenClaw plugin files not found at ${pluginDir}`);
1384
1440
  }
@@ -1389,44 +1445,103 @@ async function cmdSetupOpenClaw(args: string[]) {
1389
1445
  die(`Plugin package.json not found at ${pluginDir}/package.json — required for OpenClaw v2026.4.11+ discovery`);
1390
1446
  }
1391
1447
 
1392
- // Create extensions directory
1393
- if (!existsSync(extensionsDir)) {
1394
- mkdirSync(extensionsDir, { recursive: true });
1395
- }
1396
-
1397
- // Remove any stale install (symlink or directory) before re-installing.
1398
- // OpenClaw v2026.4.11+ discovery (discoverInDirectory in ids-*.js) uses
1399
- // readdirSync({ withFileTypes: true }) where symlinks report
1400
- // isDirectory() === false and get silently skipped, so copy mode is the
1401
- // default. The --link flag keeps symlink behavior for older OpenClaw
1402
- // versions or local development workflows where editing the live source
1403
- // should take effect without re-running setup.
1404
- try {
1405
- const { lstatSync, unlinkSync, rmSync } = await import("fs");
1406
- const stat = lstatSync(linkPath);
1407
- if (stat.isSymbolicLink()) {
1408
- unlinkSync(linkPath);
1409
- console.log(`${c.dim}Replaced stale symlink at ${linkPath}${c.reset}`);
1410
- } else if (stat.isDirectory()) {
1411
- rmSync(linkPath, { recursive: true });
1412
- console.log(`${c.dim}Replaced existing directory at ${linkPath}${c.reset}`);
1448
+ // §28.1 H1/H2: choose path. Path 1 = openclaw plugins install delegation;
1449
+ // Path 3 = direct-copy fallback honoring OPENCLAW_STATE_DIR.
1450
+ let delegated = false;
1451
+ if (hasOpenClawCli) {
1452
+ // Path 1: delegate to OpenClaw. Auto-enables, writes install records,
1453
+ // applies slot selection, refreshes registry.
1454
+ if (linkMode) {
1455
+ // §28.1 H2 link-mode: OpenClaw rejects --force with --link, so we do
1456
+ // manual stale cleanup before delegating to preserve idempotence.
1457
+ try {
1458
+ const { lstatSync, unlinkSync, rmSync } = await import("fs");
1459
+ const stat = lstatSync(linkPath);
1460
+ if (stat.isSymbolicLink()) {
1461
+ unlinkSync(linkPath);
1462
+ console.log(`${c.dim}Replaced stale symlink at ${linkPath}${c.reset}`);
1463
+ } else if (stat.isDirectory()) {
1464
+ rmSync(linkPath, { recursive: true });
1465
+ console.log(`${c.dim}Replaced existing directory at ${linkPath}${c.reset}`);
1466
+ }
1467
+ } catch (e: any) {
1468
+ if (e.code !== "ENOENT") throw e;
1469
+ }
1470
+ const r = Bun.spawnSync(
1471
+ ["openclaw", "plugins", "install", pluginDir, "-l"],
1472
+ { stdout: "inherit", stderr: "inherit" },
1473
+ );
1474
+ if (r.exitCode !== 0) {
1475
+ die(`openclaw plugins install -l failed (exit ${r.exitCode}); aborting setup`);
1476
+ }
1477
+ // OpenClaw's `plugins install -l` records the source path in
1478
+ // plugins.load.paths and persists a path install record (not a
1479
+ // filesystem symlink). The v2026.4.11 symlink-discovery skip does
1480
+ // NOT apply to this mode — discovery uses the load-path entry.
1481
+ console.log(`${c.green}Linked local plugin path via openclaw plugins install -l (profile-aware, auto-enabled)${c.reset}`);
1482
+ console.log(`${c.dim} Source recorded in plugins.load.paths — edits to ${pluginDir} take effect on next gateway restart.${c.reset}`);
1413
1483
  } else {
1414
- die(`${linkPath} exists but is not a symlink or directory. Remove it manually and re-run setup.`);
1484
+ // §28.1 H2 copy-mode: --force makes OpenClaw replace existing target,
1485
+ // preserving idempotence across reruns.
1486
+ const r = Bun.spawnSync(
1487
+ ["openclaw", "plugins", "install", pluginDir, "--force"],
1488
+ { stdout: "inherit", stderr: "inherit" },
1489
+ );
1490
+ if (r.exitCode !== 0) {
1491
+ die(`openclaw plugins install --force failed (exit ${r.exitCode}); aborting setup`);
1492
+ }
1493
+ console.log(`${c.green}Installed plugin via openclaw plugins install --force (profile-aware, auto-enabled)${c.reset}`);
1415
1494
  }
1416
- } catch (e: any) {
1417
- if (e.code !== "ENOENT") throw e;
1418
- }
1419
-
1420
- if (linkMode) {
1421
- const { symlinkSync } = await import("fs");
1422
- symlinkSync(pluginDir, linkPath);
1423
- console.log(`${c.green}Installed plugin: ${linkPath} → ${pluginDir} (symlink)${c.reset}`);
1424
- console.log(`${c.yellow} Warning: symlink mode. OpenClaw v2026.4.11+ discovery skips${c.reset}`);
1425
- console.log(`${c.yellow} symlinks silently. Re-run without --link on current releases.${c.reset}`);
1495
+ delegated = true;
1426
1496
  } else {
1427
- const { cpSync } = await import("fs");
1428
- cpSync(pluginDir, linkPath, { recursive: true, dereference: true });
1429
- console.log(`${c.green}Installed plugin: ${linkPath} (copied from ${pluginDir})${c.reset}`);
1497
+ // Path 3: direct-copy fallback. Honors OPENCLAW_STATE_DIR via the
1498
+ // resolveExtensionsDirNoOpenClaw helper. Profile awareness is limited
1499
+ // to env vars (no manifest validation, no security scan, no install
1500
+ // records) — surface that to the user.
1501
+ console.log(`${c.yellow}openclaw CLI not on PATH — using direct-copy install.${c.reset}`);
1502
+ console.log(`${c.yellow} Profile awareness limited to OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH${c.reset}`);
1503
+ console.log(`${c.yellow} env vars. Install OpenClaw to enable manifest validation, security${c.reset}`);
1504
+ console.log(`${c.yellow} scans, and full plugin lifecycle management.${c.reset}`);
1505
+
1506
+ // Create extensions directory.
1507
+ if (!existsSync(extensionsDir)) {
1508
+ mkdirSync(extensionsDir, { recursive: true });
1509
+ }
1510
+
1511
+ // Remove any stale install (symlink or directory) before re-installing.
1512
+ // OpenClaw v2026.4.11+ discovery (discoverInDirectory in ids-*.js) uses
1513
+ // readdirSync({ withFileTypes: true }) where symlinks report
1514
+ // isDirectory() === false and get silently skipped, so copy mode is the
1515
+ // default. The --link flag keeps symlink behavior for older OpenClaw
1516
+ // versions or local development workflows where editing the live source
1517
+ // should take effect without re-running setup.
1518
+ try {
1519
+ const { lstatSync, unlinkSync, rmSync } = await import("fs");
1520
+ const stat = lstatSync(linkPath);
1521
+ if (stat.isSymbolicLink()) {
1522
+ unlinkSync(linkPath);
1523
+ console.log(`${c.dim}Replaced stale symlink at ${linkPath}${c.reset}`);
1524
+ } else if (stat.isDirectory()) {
1525
+ rmSync(linkPath, { recursive: true });
1526
+ console.log(`${c.dim}Replaced existing directory at ${linkPath}${c.reset}`);
1527
+ } else {
1528
+ die(`${linkPath} exists but is not a symlink or directory. Remove it manually and re-run setup.`);
1529
+ }
1530
+ } catch (e: any) {
1531
+ if (e.code !== "ENOENT") throw e;
1532
+ }
1533
+
1534
+ if (linkMode) {
1535
+ const { symlinkSync } = await import("fs");
1536
+ symlinkSync(pluginDir, linkPath);
1537
+ console.log(`${c.green}Installed plugin: ${linkPath} → ${pluginDir} (symlink)${c.reset}`);
1538
+ console.log(`${c.yellow} Warning: symlink mode. OpenClaw v2026.4.11+ discovery skips${c.reset}`);
1539
+ console.log(`${c.yellow} symlinks silently. Re-run without --link on current releases.${c.reset}`);
1540
+ } else {
1541
+ const { cpSync } = await import("fs");
1542
+ cpSync(pluginDir, linkPath, { recursive: true, dereference: true });
1543
+ console.log(`${c.green}Installed plugin: ${linkPath} (copied from ${pluginDir})${c.reset}`);
1544
+ }
1430
1545
  }
1431
1546
 
1432
1547
  // ----- §14.3 upgrade migration -----
@@ -1472,30 +1587,45 @@ async function cmdSetupOpenClaw(args: string[]) {
1472
1587
  console.log(`have a bug where plugins.slots.contextEngine is silently dropped`);
1473
1588
  console.log(`during config normalization (openclaw/openclaw#64192).`);
1474
1589
 
1475
- // Remaining steps. CLI discovery finds the plugin immediately because the
1476
- // plugin dir now ships a package.json with openclaw.extensions declared, so
1477
- // `openclaw plugins enable clawmem` can run before any gateway restart.
1478
- // The enable command switches the exclusive memory slot to clawmem and
1479
- // disables memory-core/memory-lancedb automatically. Then the gateway
1480
- // restart applies the new slot assignment.
1590
+ // §28.1 H1: dual next-steps output. Path 1 (delegated) auto-enables via
1591
+ // persistPluginInstall, so the legacy "Step 1: enable" instruction is
1592
+ // redundant and would mislead users. Path 3 (direct copy) writes only
1593
+ // the plugin files; the user must still run `openclaw plugins enable`
1594
+ // themselves, so the original 4-step output is preserved verbatim.
1481
1595
  console.log();
1482
1596
  console.log(`${c.bold}Next steps:${c.reset}`);
1483
1597
  console.log();
1484
- console.log(` 1. Enable ClawMem as the active memory plugin:`);
1485
- console.log(` ${c.cyan}openclaw plugins enable clawmem${c.reset}`);
1486
- console.log(` ${c.dim}(Switches plugins.slots.memory to clawmem and disables memory-core if active.)${c.reset}`);
1487
- console.log();
1488
- console.log(` 2. Restart the gateway to apply:`);
1489
- console.log(` ${c.cyan}openclaw gateway restart${c.reset}`);
1490
- console.log();
1491
- console.log(` 3. Configure GPU endpoints (if not using defaults):`);
1492
- console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuEmbed http://YOUR_GPU:8088${c.reset}`);
1493
- console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlm http://YOUR_GPU:8089${c.reset}`);
1494
- console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlmModel qwen3${c.reset}`);
1495
- console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuRerank http://YOUR_GPU:8090${c.reset}`);
1496
- console.log();
1497
- console.log(` 4. Start the REST API (for agent tools):`);
1498
- console.log(` ${c.cyan}clawmem serve &${c.reset}`);
1598
+ if (delegated) {
1599
+ // Path 1 — plugin already enabled and registered by openclaw plugins install.
1600
+ console.log(` 1. Restart the gateway to apply:`);
1601
+ console.log(` ${c.cyan}openclaw gateway restart${c.reset}`);
1602
+ console.log();
1603
+ console.log(` 2. Configure GPU endpoints (if not using defaults):`);
1604
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuEmbed http://YOUR_GPU:8088${c.reset}`);
1605
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlm http://YOUR_GPU:8089${c.reset}`);
1606
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlmModel qwen3${c.reset}`);
1607
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuRerank http://YOUR_GPU:8090${c.reset}`);
1608
+ console.log();
1609
+ console.log(` 3. Start the REST API (for agent tools):`);
1610
+ console.log(` ${c.cyan}clawmem serve &${c.reset}`);
1611
+ } else {
1612
+ // Path 3 — direct-copy install. User still needs to enable + restart.
1613
+ console.log(` 1. Enable ClawMem as the active memory plugin:`);
1614
+ console.log(` ${c.cyan}openclaw plugins enable clawmem${c.reset}`);
1615
+ console.log(` ${c.dim}(Switches plugins.slots.memory to clawmem and disables memory-core if active.)${c.reset}`);
1616
+ console.log();
1617
+ console.log(` 2. Restart the gateway to apply:`);
1618
+ console.log(` ${c.cyan}openclaw gateway restart${c.reset}`);
1619
+ console.log();
1620
+ console.log(` 3. Configure GPU endpoints (if not using defaults):`);
1621
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuEmbed http://YOUR_GPU:8088${c.reset}`);
1622
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlm http://YOUR_GPU:8089${c.reset}`);
1623
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuLlmModel qwen3${c.reset}`);
1624
+ console.log(` ${c.cyan}openclaw config set plugins.entries.clawmem.config.gpuRerank http://YOUR_GPU:8090${c.reset}`);
1625
+ console.log();
1626
+ console.log(` 4. Start the REST API (for agent tools):`);
1627
+ console.log(` ${c.cyan}clawmem serve &${c.reset}`);
1628
+ }
1499
1629
  console.log();
1500
1630
  console.log(`${c.bold}Important: keep dreaming disabled${c.reset}`);
1501
1631
  console.log(` ClawMem runs its own consolidation workers (CLAWMEM_ENABLE_CONSOLIDATION`);
@@ -2844,7 +2974,7 @@ ${c.bold}Setup:${c.reset}
2844
2974
  clawmem collection remove <name>
2845
2975
  clawmem setup hooks [--remove] Install/remove Claude Code hooks
2846
2976
  clawmem setup mcp [--remove] Register/remove MCP in ~/.claude.json
2847
- clawmem setup openclaw [--remove] Show OpenClaw plugin installation steps
2977
+ clawmem setup openclaw [--link] [--remove] Install/remove ClawMem as OpenClaw memory plugin
2848
2978
  clawmem install-service [--enable] Install systemd watcher service
2849
2979
 
2850
2980
  ${c.bold}Indexing:${c.reset}
@@ -7,7 +7,15 @@
7
7
 
8
8
  import { execFile, spawn, type ChildProcess } from "node:child_process";
9
9
  import { existsSync } from "node:fs";
10
- import { resolve } from "node:path";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ // ESM equivalent of CommonJS __dirname. This package declares
14
+ // "type": "module", so __dirname is not defined when loaded by a plain
15
+ // Node.js ESM loader (e.g. OpenClaw's plugin host). Bun shims __dirname
16
+ // in ESM, which is why this regression is invisible under `bun test`.
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
11
19
 
12
20
  // =============================================================================
13
21
  // Types
@@ -0,0 +1,244 @@
1
+ /**
2
+ * OpenClaw path-resolution helpers + setup help text for `clawmem setup openclaw`.
3
+ *
4
+ * §28.1 (issue #11): historically `cmdSetupOpenClaw` hardcoded
5
+ * `~/.openclaw/extensions/clawmem` and ignored `OPENCLAW_STATE_DIR`. v0.10.4
6
+ * delegates to `openclaw plugins install` when the CLI is on PATH (which
7
+ * inherits OpenClaw's own `resolveConfigDir(env)` semantics) and falls back
8
+ * to a direct-copy install otherwise. The fallback path needs to mirror
9
+ * OpenClaw's `resolveConfigDir` precisely for env-var-honoring custom
10
+ * profile installs to land in the expected directory.
11
+ *
12
+ * Mirrors: openclaw/src/utils.ts:119 (resolveConfigDir) and
13
+ * openclaw/src/infra/home-dir.ts (home resolution).
14
+ *
15
+ * Helpers take injected `env` + `homedir` so they are testable in isolation
16
+ * without needing to touch process.env or the real os.homedir.
17
+ */
18
+
19
+ import { homedir as defaultHomedir } from "node:os";
20
+ import { dirname, resolve as pathResolve } from "node:path";
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ export type EnvLike = Record<string, string | undefined>;
27
+ export type HomedirFn = () => string;
28
+
29
+ export interface PathResolverOpts {
30
+ env?: EnvLike;
31
+ homedir?: HomedirFn;
32
+ }
33
+
34
+ // =============================================================================
35
+ // Trim / normalize
36
+ // =============================================================================
37
+
38
+ /**
39
+ * Plain trim — used for OPENCLAW_STATE_DIR and OPENCLAW_CONFIG_PATH so the
40
+ * fallback resolver mirrors OpenClaw's `resolveConfigDir` (utils.ts:119)
41
+ * EXACTLY. OpenClaw applies only `.trim()` to those env vars, so a value
42
+ * of `"undefined"` or `"null"` is treated as a literal directory name (the
43
+ * user shot themselves in the foot, but ClawMem must agree with OpenClaw
44
+ * about WHERE the foot is shot — diverging here would mean Path 1 and
45
+ * Path 3 install into different locations for the same env, which is
46
+ * exactly the bug class §28.1 set out to fix).
47
+ */
48
+ export function plainTrim(value: string | undefined): string | undefined {
49
+ if (!value) return undefined;
50
+ const t = value.trim();
51
+ return t || undefined;
52
+ }
53
+
54
+ /**
55
+ * Mirrors OpenClaw's `home-dir.ts:normalize`. Treats empty strings,
56
+ * whitespace-only strings, and the literal strings "undefined" / "null" as
57
+ * unset. Used ONLY for home-resolution env vars (OPENCLAW_HOME, HOME,
58
+ * USERPROFILE) to match OpenClaw's home-dir helper; do NOT use this for
59
+ * OPENCLAW_STATE_DIR or OPENCLAW_CONFIG_PATH (see plainTrim).
60
+ */
61
+ export function trim(value: string | undefined): string | undefined {
62
+ if (!value) return undefined;
63
+ const t = value.trim();
64
+ if (!t || t === "undefined" || t === "null") return undefined;
65
+ return t;
66
+ }
67
+
68
+ // =============================================================================
69
+ // Home resolution
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Mirrors `openclaw/src/infra/home-dir.ts` resolution priority:
74
+ * OPENCLAW_HOME → HOME → USERPROFILE → os.homedir() → path.resolve(cwd())
75
+ *
76
+ * `OPENCLAW_HOME` itself can begin with a tilde, in which case we expand it
77
+ * against the *next* fallback (HOME / USERPROFILE / os.homedir).
78
+ */
79
+ export function resolveHomeForOpenClaw(opts: PathResolverOpts = {}): string {
80
+ const env = opts.env ?? process.env;
81
+ const homedir = opts.homedir ?? defaultHomedir;
82
+
83
+ const explicitHome = trim(env.OPENCLAW_HOME);
84
+ if (explicitHome) {
85
+ if (
86
+ explicitHome === "~" ||
87
+ explicitHome.startsWith("~/") ||
88
+ explicitHome.startsWith("~\\")
89
+ ) {
90
+ const fallback = resolveOsHome(env, homedir);
91
+ if (fallback) {
92
+ return pathResolve(explicitHome.replace(/^~(?=$|[\\/])/, fallback));
93
+ }
94
+ // No fallback available; fall through to other priorities below.
95
+ } else {
96
+ return pathResolve(explicitHome);
97
+ }
98
+ }
99
+
100
+ const osHome = resolveOsHome(env, homedir);
101
+ if (osHome) return pathResolve(osHome);
102
+
103
+ // Last-resort: cwd. Matches `resolveRequiredHomeDir` in OpenClaw.
104
+ return pathResolve(process.cwd());
105
+ }
106
+
107
+ function resolveOsHome(env: EnvLike, homedir: HomedirFn): string | undefined {
108
+ const homeEnv = trim(env.HOME);
109
+ if (homeEnv) return homeEnv;
110
+ const userProfile = trim(env.USERPROFILE);
111
+ if (userProfile) return userProfile;
112
+ try {
113
+ const safe = trim(homedir());
114
+ if (safe) return safe;
115
+ } catch {
116
+ // os.homedir() can throw on misconfigured systems
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ // =============================================================================
122
+ // Tilde expansion
123
+ // =============================================================================
124
+
125
+ /**
126
+ * Expands a leading `~`, `~/`, or `~\` against the OpenClaw home resolver.
127
+ * Does NOT support `~user/...` (other-user expansion) — neither does
128
+ * OpenClaw's `expandHomePrefix`.
129
+ */
130
+ export function expandHome(input: string, opts: PathResolverOpts = {}): string {
131
+ if (!input.startsWith("~")) return input;
132
+ if (
133
+ input !== "~" &&
134
+ !input.startsWith("~/") &&
135
+ !input.startsWith("~\\")
136
+ ) {
137
+ // Looks like `~user` or some other non-home tilde form; leave untouched.
138
+ return input;
139
+ }
140
+ const home = resolveHomeForOpenClaw(opts);
141
+ return input.replace(/^~(?=$|[\\/])/, home);
142
+ }
143
+
144
+ // =============================================================================
145
+ // Extensions directory resolver (CLI-absent fallback)
146
+ // =============================================================================
147
+
148
+ /**
149
+ * Resolves the `extensions/` directory we should write into when `openclaw`
150
+ * CLI is not on PATH. Mirrors `openclaw/src/utils.ts:119 resolveConfigDir`:
151
+ * 1. OPENCLAW_STATE_DIR overrides everything
152
+ * 2. OPENCLAW_CONFIG_PATH → config root = dirname(file)
153
+ * 3. <home>/.openclaw
154
+ *
155
+ * The result is `pathResolve`d so callers can compare against equally
156
+ * normalized paths.
157
+ */
158
+ export function resolveExtensionsDirNoOpenClaw(
159
+ opts: PathResolverOpts = {},
160
+ ): string {
161
+ const env = opts.env ?? process.env;
162
+
163
+ // OPENCLAW_STATE_DIR + OPENCLAW_CONFIG_PATH use plainTrim (no
164
+ // "undefined"/"null" filtering) to match OpenClaw's resolveConfigDir
165
+ // exactly. See plainTrim docstring.
166
+ const stateDir = plainTrim(env.OPENCLAW_STATE_DIR);
167
+ if (stateDir) {
168
+ return pathResolve(expandHome(stateDir, opts), "extensions");
169
+ }
170
+
171
+ const configPath = plainTrim(env.OPENCLAW_CONFIG_PATH);
172
+ if (configPath) {
173
+ return pathResolve(dirname(expandHome(configPath, opts)), "extensions");
174
+ }
175
+
176
+ return pathResolve(resolveHomeForOpenClaw(opts), ".openclaw", "extensions");
177
+ }
178
+
179
+ // =============================================================================
180
+ // `clawmem setup openclaw --help` text
181
+ // =============================================================================
182
+
183
+ /**
184
+ * Prints help for `clawmem setup openclaw`. Documents flags, env vars
185
+ * consulted, and the CLI-delegation behavior introduced in v0.10.4 (§28.1).
186
+ */
187
+ export function printSetupOpenClawHelp(): void {
188
+ const lines = [
189
+ "",
190
+ "clawmem setup openclaw [--link] [--remove] [--help|-h]",
191
+ "",
192
+ " Install ClawMem as an OpenClaw memory plugin.",
193
+ "",
194
+ " When the openclaw CLI is on PATH, this command delegates to",
195
+ " `openclaw plugins install <pluginDir>`, which auto-enables the plugin,",
196
+ " writes install records, and applies slot selection. Otherwise it falls",
197
+ " back to a direct-copy install honoring OPENCLAW_STATE_DIR.",
198
+ "",
199
+ "Flags:",
200
+ " --link Install in load-path mode instead of copying files.",
201
+ " When openclaw is on PATH, delegates to `openclaw plugins",
202
+ " install -l <path>` which records the source in",
203
+ " plugins.load.paths (NOT a filesystem symlink). When",
204
+ " openclaw is absent, falls back to a real symlink at",
205
+ " <extensions>/clawmem (note: OpenClaw v2026.4.11+",
206
+ " discovery silently skips symlinked plugins in the",
207
+ " fallback path, so prefer the delegated path).",
208
+ " --remove Uninstall ClawMem from the OpenClaw extensions dir.",
209
+ " Tries `openclaw plugins uninstall clawmem --force` first;",
210
+ " falls back to manual cleanup at the resolved extensions",
211
+ " path for legacy unmanaged installs.",
212
+ " --help, -h Print this message and exit.",
213
+ "",
214
+ "Environment variables (consulted by the CLI-absent fallback path and",
215
+ "inherited by the openclaw subprocess in the delegation path):",
216
+ " OPENCLAW_STATE_DIR Override the OpenClaw config root. Plugin",
217
+ " installs into <OPENCLAW_STATE_DIR>/extensions/",
218
+ " clawmem.",
219
+ " OPENCLAW_CONFIG_PATH Override the OpenClaw config file path; root",
220
+ " becomes dirname(OPENCLAW_CONFIG_PATH).",
221
+ " OPENCLAW_HOME Override the home directory used to resolve",
222
+ " the default ~/.openclaw root.",
223
+ " HOME / USERPROFILE Standard home-dir env vars; consulted in that",
224
+ " order when OPENCLAW_HOME is unset.",
225
+ "",
226
+ "Examples:",
227
+ " clawmem setup openclaw",
228
+ " Install with default profile.",
229
+ "",
230
+ " OPENCLAW_STATE_DIR=~/.openclaw-dev clawmem setup openclaw",
231
+ " Install into the `dev` profile (~/.openclaw-dev/extensions/clawmem).",
232
+ "",
233
+ " clawmem setup openclaw --link",
234
+ " Load-path mode (delegated install) or symlink (fallback) — local",
235
+ " development workflow where edits to the source dir take effect.",
236
+ "",
237
+ " clawmem setup openclaw --remove",
238
+ " Uninstall ClawMem and reset OpenClaw memory slot.",
239
+ "",
240
+ ];
241
+ for (const line of lines) {
242
+ console.log(line);
243
+ }
244
+ }