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 +1 -0
- package/CLAUDE.md +1 -0
- package/README.md +16 -7
- package/package.json +1 -1
- package/src/clawmem.ts +202 -72
- package/src/openclaw/shell.ts +9 -1
- package/src/openclaw-paths.ts +244 -0
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
|
-
|
|
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
|
|
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
|
|
1106
|
-
#
|
|
1107
|
-
#
|
|
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
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1353
|
-
console.log(`${c.green}Removed plugin
|
|
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
|
-
|
|
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
|
-
//
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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]
|
|
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}
|
package/src/openclaw/shell.ts
CHANGED
|
@@ -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
|
+
}
|