claude-mem-lite 3.1.0 → 3.1.1

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.1.0",
13
+ "version": "3.1.1",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/README.md CHANGED
@@ -160,7 +160,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
160
160
 
161
161
  Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
162
162
 
163
- > **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by absolute path (`node ~/.claude-mem-lite/cli.mjs <cmd>`), so they work without anything on your `PATH`. A global `claude-mem-lite` **shell** command (for running queries yourself in a terminal) is **optional** — `npm i -g claude-mem-lite` — and is a *separate* npm install: the plugin's auto-update does **not** refresh it, so re-run `npm i -g claude-mem-lite@latest` if you want that shell command kept in sync. You do **not** need it for the plugin to be fully functional.
163
+ > **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (`${CLAUDE_PLUGIN_ROOT}/cli.mjs <cmd>`), so they work without anything on your `PATH`. A global `claude-mem-lite` **shell** command (for running queries yourself in a terminal) is **optional** — `npm i -g claude-mem-lite` — and is a *separate* npm install: the plugin's auto-update does **not** refresh it, so re-run `npm i -g claude-mem-lite@latest` if you want that shell command kept in sync. You do **not** need it for the plugin to be fully functional.
164
164
 
165
165
  > **Auto-adopt fires on the first SessionStart per project (v2.82.1+).** The plugin automatically writes the **invited-memory sentinel** (a system-authority pointer that boosts Claude's proactive use of `mem_recall` / `mem_save`) into the project's memdir — **no manual `/adopt` needed**, regardless of install path (npm, npx, `/plugin`, manual). Per-project opt-out: `claude-mem-lite adopt --disable` (`--enable` to re-arm). Global opt-out: `export MEM_NO_AUTO_ADOPT=1`. Manual `/adopt` remains available for re-applying after edits and for the `--all` batch path.
166
166
 
@@ -263,9 +263,9 @@ surface — reach them through the CLI column in the second table.
263
263
  | `mem_update` | `claude-mem-lite update <id>` | Edit an observation in place. |
264
264
  | `mem_stats` | `claude-mem-lite stats` | Counts, type distribution, daily activity. |
265
265
  | `mem_delete` | `claude-mem-lite delete <id>` | Preview / confirm workflow, FTS5 cleanup. |
266
- | `mem_compress` | `claude-mem-lite compress --preview` | Roll up old low-value observations. |
267
- | `mem_maintain` | `claude-mem-lite maintain --action scan` | dedup / decay / cleanup / rebuild_vectors. |
268
- | `mem_optimize` | `claude-mem-lite optimize --action preview` | LLM-powered re-enrich / normalize / cluster-merge. |
266
+ | `mem_compress` | `claude-mem-lite compress` | Roll up old low-value observations (preview default; `--execute` to apply). |
267
+ | `mem_maintain` | `claude-mem-lite maintain scan --ops dedup,decay` | dedup / decay / cleanup / rebuild_vectors (`scan` previews, `execute` applies). |
268
+ | `mem_optimize` | `claude-mem-lite optimize` | LLM-powered re-enrich / normalize / cluster-merge (preview default; `--run` to apply). |
269
269
  | `mem_export` | `claude-mem-lite export` | JSON / JSONL dump, filters by project, type, date. |
270
270
  | `mem_fts_check` | `claude-mem-lite fts-check [--rebuild]` | FTS5 integrity + rebuild. |
271
271
  | `mem_browse` | `claude-mem-lite browse` | Tier-grouped dashboard (working / active / archive). |
package/README.zh-CN.md CHANGED
@@ -127,6 +127,8 @@
127
127
 
128
128
  插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
129
129
 
130
+ > **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI(`${CLAUDE_PLUGIN_ROOT}/cli.mjs <cmd>`),因此不依赖 `PATH` 上的任何东西。全局 `claude-mem-lite` **shell** 命令(用于你自己在终端里跑查询)是**可选**的 —— `npm i -g claude-mem-lite` —— 且是**独立**的 npm 安装:插件的自动更新**不会**刷新它,想保持同步就重新跑 `npm i -g claude-mem-lite@latest`。插件要完整工作**并不需要**它。
131
+
130
132
  ### 方式二:npx(一行命令)
131
133
 
132
134
  ```bash
@@ -223,9 +225,9 @@ v2.34.0 起服务端注册 17 个工具,但 `tools/list` 只暴露 6 个 **核
223
225
  | `mem_update` | `claude-mem-lite update <id>` | 原地更新某条观察。 |
224
226
  | `mem_stats` | `claude-mem-lite stats` | 计数、类型分布、每日活动。 |
225
227
  | `mem_delete` | `claude-mem-lite delete <id>` | 预览 / 确认流程,FTS5 自动清理。 |
226
- | `mem_compress` | `claude-mem-lite compress --preview` | 压缩旧的低价值观察。 |
227
- | `mem_maintain` | `claude-mem-lite maintain --action scan` | 去重 / decay / 清理 / 向量重建。 |
228
- | `mem_optimize` | `claude-mem-lite optimize --action preview` | LLM 深度优化:re-enrich / normalize / cluster-merge |
228
+ | `mem_compress` | `claude-mem-lite compress` | 压缩旧的低价值观察(默认 preview;`--execute` 执行)。 |
229
+ | `mem_maintain` | `claude-mem-lite maintain scan --ops dedup,decay` | 去重 / decay / 清理 / 向量重建(`scan` 预览,`execute` 执行)。 |
230
+ | `mem_optimize` | `claude-mem-lite optimize` | LLM 深度优化:re-enrich / normalize / cluster-merge(默认 preview;`--run` 执行)。 |
229
231
  | `mem_export` | `claude-mem-lite export` | JSON / JSONL 导出,支持项目/类型/日期过滤。 |
230
232
  | `mem_fts_check` | `claude-mem-lite fts-check [--rebuild]` | FTS5 完整性检查与重建。 |
231
233
  | `mem_browse` | `claude-mem-lite browse` | 分层仪表盘(working / active / archive)。 |
package/adopt-content.mjs CHANGED
@@ -7,6 +7,8 @@
7
7
  // docs/plans/2026-04-16-invited-memory-pattern.md so installs do a version-
8
8
  // bump replace instead of treating the old content as a user edit.
9
9
 
10
+ import { CLI_INVOKE } from './cli-path.mjs';
11
+
10
12
  export const PLUGIN_SLUG = 'claude-mem-lite';
11
13
  export const CURRENT_SENTINEL_VERSION = 'v1';
12
14
 
@@ -26,7 +28,7 @@ export function getIndexLine() {
26
28
  export function getDetailDoc() {
27
29
  return `# claude-mem-lite 插件契约
28
30
 
29
- > 由 \`node ~/.claude-mem-lite/cli.mjs adopt\` 生成;卸载用 \`node ~/.claude-mem-lite/cli.mjs unadopt\`。
31
+ > 由 \`${CLI_INVOKE} adopt\` 生成;卸载用 \`${CLI_INVOKE} unadopt\`。
30
32
  > 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
31
33
 
32
34
  ## 何时调用 MCP 工具
@@ -58,37 +60,37 @@ MCP 层,按名 \`tools/call\` 仍可命中,但对 Claude Code 这类只读 t
58
60
 
59
61
  | 场景 | CLI |
60
62
  |------|-----|
61
- | 清理过期记忆 | \`node ~/.claude-mem-lite/cli.mjs maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale\` |
62
- | 深度优化(Haiku) | \`node ~/.claude-mem-lite/cli.mjs optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\` 选阶段) |
63
- | 压缩旧条目 | \`node ~/.claude-mem-lite/cli.mjs compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\` 改阈值) |
64
- | FTS5 索引检查 / 重建 | \`node ~/.claude-mem-lite/cli.mjs fts-check [--rebuild]\` |
65
- | tier 分组浏览 | \`node ~/.claude-mem-lite/cli.mjs browse [--tier active]\` |
66
- | 导出 JSON/JSONL | \`node ~/.claude-mem-lite/cli.mjs export [--format jsonl]\` |
67
- | 统计总量 / 健康 | \`node ~/.claude-mem-lite/cli.mjs stats [--days 30]\` |
68
- | 删除某条 | \`node ~/.claude-mem-lite/cli.mjs delete <id>[,<id>]\` |
69
- | 更新某条 | \`node ~/.claude-mem-lite/cli.mjs update <id> [--title ...]\` |
70
- | 列 / 搜索 / 导入 skill-agent registry | \`node ~/.claude-mem-lite/cli.mjs registry <list\\|search\\|import>\` |
63
+ | 清理过期记忆 | \`${CLI_INVOKE} maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale --confirm\`(execute 删行必须带 \`--confirm\`,否则只预览) |
64
+ | 深度优化(Haiku) | \`${CLI_INVOKE} optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\` 选阶段) |
65
+ | 压缩旧条目 | \`${CLI_INVOKE} compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\` 改阈值) |
66
+ | FTS5 索引检查 / 重建 | \`${CLI_INVOKE} fts-check [--rebuild]\` |
67
+ | tier 分组浏览 | \`${CLI_INVOKE} browse [--tier active]\` |
68
+ | 导出 JSON/JSONL | \`${CLI_INVOKE} export [--format jsonl]\` |
69
+ | 统计总量 / 健康 | \`${CLI_INVOKE} stats [--days 30]\` |
70
+ | 删除某条 | \`${CLI_INVOKE} delete <id>[,<id>]\` |
71
+ | 更新某条 | \`${CLI_INVOKE} update <id> [--title ...]\` |
72
+ | 列 / 搜索 / 导入 skill-agent registry | \`${CLI_INVOKE} registry <list\\|search\\|import>\` |
71
73
  | 按 registry 名载入 skill/agent | (MCP only:\`mem_use\`;由用户主动请求时才使用) |
72
74
 
73
75
  ## CLI 速查(常用检索)
74
76
 
75
77
  | 命令 | 用途 |
76
78
  |------|------|
77
- | \`node ~/.claude-mem-lite/cli.mjs search "query"\` | FTS5 全文搜索 |
78
- | \`node ~/.claude-mem-lite/cli.mjs search "err" --type bugfix\` | 按类型过滤 |
79
- | \`node ~/.claude-mem-lite/cli.mjs recall "file.mjs"\` | 文件相关记忆 |
80
- | \`node ~/.claude-mem-lite/cli.mjs recent 5\` | 最近 5 条 |
81
- | \`node ~/.claude-mem-lite/cli.mjs get 42,43\` | 按 ID 展开 |
82
- | \`node ~/.claude-mem-lite/cli.mjs timeline --anchor 42\` | 时间线上下文 |
79
+ | \`${CLI_INVOKE} search "query"\` | FTS5 全文搜索 |
80
+ | \`${CLI_INVOKE} search "err" --type bugfix\` | 按类型过滤 |
81
+ | \`${CLI_INVOKE} recall "file.mjs"\` | 文件相关记忆 |
82
+ | \`${CLI_INVOKE} recent 5\` | 最近 5 条 |
83
+ | \`${CLI_INVOKE} get 42,43\` | 按 ID 展开 |
84
+ | \`${CLI_INVOKE} timeline --anchor 42\` | 时间线上下文 |
83
85
 
84
86
  ## 质量门槛
85
87
 
86
88
  - \`mem_save\` 的 \`lesson_learned\` 不要写 \`none\`——写不出教训就保持 NULL
87
- - \`decision\` 命中率 72.7% vs \`change\` 16.5%——一条好 decision 20 条 change
89
+ - \`decision\` 的命中率高于 \`change\`(当前遥测约 3:1,数值会漂移——用 \`${CLI_INVOKE} stats\` 实测,别套固定倍数);方向稳健:一条好 decision 抵数条 change
88
90
  - 一般搜索跳过 \`obs_type\` 让系统自动路由;特定意图再过滤
89
91
 
90
92
  ## 卸载
91
93
 
92
- \`node ~/.claude-mem-lite/cli.mjs unadopt\` 精确移除 sentinel 段 + 本文件;其它 MEMORY.md 内容不动。
94
+ \`${CLI_INVOKE} unadopt\` 精确移除 sentinel 段 + 本文件;其它 MEMORY.md 内容不动。
93
95
  `;
94
96
  }
package/cli-path.mjs ADDED
@@ -0,0 +1,21 @@
1
+ // cli-path.mjs — single source of truth for invoking the bundled CLI by an
2
+ // absolute, install-shape-independent path.
3
+ //
4
+ // cli.mjs is a sibling of this module at the package root, so import.meta.url
5
+ // resolves it correctly on EVERY install shape: plugin cache, `npm i -g`
6
+ // symlink farm, and manual/dev checkout. The pre-v3.1.1 hardcoded
7
+ // `~/.claude-mem-lite/cli.mjs` only existed on direct-install symlink farms —
8
+ // on a plugin-only install setup.sh provisions the data dir but never
9
+ // materializes source there, so that path is a module-not-found. See the
10
+ // 2026-06-20 code review, findings #1/#2/#3/#13.
11
+ //
12
+ // Use this for JS-emitted, runtime-resolved surfaces (MCP `instructions`, the
13
+ // per-tool "Equivalent CLI" hints, hook recovery lines, the generated adopt
14
+ // doc). Plugin MANIFEST files (commands/*.md, .mcp.json) must instead use the
15
+ // literal `${CLAUDE_PLUGIN_ROOT}` token, which Claude Code — not the shell —
16
+ // substitutes at execution time (the env var is absent from a plain Bash env).
17
+
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ export const CLI_PATH = fileURLToPath(new URL('./cli.mjs', import.meta.url));
21
+ export const CLI_INVOKE = `node ${CLI_PATH}`;
package/commands/adopt.md CHANGED
@@ -57,4 +57,4 @@ session. After running `adopt`:
57
57
 
58
58
  Same caveat applies in reverse for `/unadopt`.
59
59
 
60
- !node ~/.claude-mem-lite/cli.mjs adopt $ARGUMENTS
60
+ !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs adopt $ARGUMENTS
package/commands/bug.md CHANGED
@@ -42,7 +42,7 @@ Build the body as `<description>\n\nRepro:\n<repro-steps>` (or just
42
42
 
43
43
  Run via Bash:
44
44
 
45
- node ~/.claude-mem-lite/cli.mjs activity save \
45
+ node ${CLAUDE_PLUGIN_ROOT}/cli.mjs activity save \
46
46
  --type bug \
47
47
  --title "<first 60 chars of description>" \
48
48
  --body "<body>" \
@@ -37,7 +37,7 @@ Run via Bash — the CLI takes the lesson text as positional args and stores
37
37
  the full text as the title (the `activity save` command uses title-as-body
38
38
  when `--body` is absent):
39
39
 
40
- node ~/.claude-mem-lite/cli.mjs activity save \
40
+ node ${CLAUDE_PLUGIN_ROOT}/cli.mjs activity save \
41
41
  --type lesson \
42
42
  --title "<first 60 chars of text>" \
43
43
  --body "<full text>" \
package/commands/mem.md CHANGED
@@ -23,16 +23,16 @@ Search and browse your project memory efficiently.
23
23
 
24
24
  When the user invokes `/mem`, parse their intent:
25
25
 
26
- - `/mem search <query>` → run `node ~/.claude-mem-lite/cli.mjs search <query>` via Bash
27
- - `/mem recent` or `/mem recent 20` → run `node ~/.claude-mem-lite/cli.mjs recent [N]` via Bash
28
- - `/mem recall <file>` → run `node ~/.claude-mem-lite/cli.mjs recall <file>` via Bash
29
- - `/mem timeline <id>` → run `node ~/.claude-mem-lite/cli.mjs timeline --anchor <id>` via Bash
26
+ - `/mem search <query>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
27
+ - `/mem recent` or `/mem recent 20` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recent [N]` via Bash
28
+ - `/mem recall <file>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recall <file>` via Bash
29
+ - `/mem timeline <id>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs timeline --anchor <id>` via Bash
30
30
  - `/mem save <text>` → call `mem_save` MCP tool with the text as content
31
- - `/mem stats` → run `node ~/.claude-mem-lite/cli.mjs stats` via Bash
32
- - `/mem get <ids>` → run `node ~/.claude-mem-lite/cli.mjs get <ids>` via Bash
31
+ - `/mem stats` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs stats` via Bash
32
+ - `/mem get <ids>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <ids>` via Bash
33
33
  - `/mem cleanup` → run `mem_maintain(action="scan")`, report pending purge count and stale items to user, ask for confirmation, then run `mem_maintain(action="execute", operations=["purge_stale"])` if confirmed
34
34
  - `/mem cleanup Nd` (e.g. `60d`) → same as above but use `retain_days=N` to only purge items older than N days
35
35
  - `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `retain_days=N`
36
- - `/mem <query>` (no subcommand) → treat as search, run `node ~/.claude-mem-lite/cli.mjs search <query>` via Bash
36
+ - `/mem <query>` (no subcommand) → treat as search, run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
37
37
 
38
- Use Bash commands first. For detailed data, use `node ~/.claude-mem-lite/cli.mjs get <id>` via Bash.
38
+ Use Bash commands first. For detailed data, use `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <id>` via Bash.
@@ -27,4 +27,4 @@ Once unadopted, the conservative hook layer (SessionStart `File Lessons` /
27
27
  `Key Context`, MCP instructions `WHEN TO USE`) goes back to verbose mode —
28
28
  Claude Code will see the full injection again on the next session start.
29
29
 
30
- !node ~/.claude-mem-lite/cli.mjs unadopt $ARGUMENTS
30
+ !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS
package/install.mjs CHANGED
@@ -1369,6 +1369,23 @@ async function doctor() {
1369
1369
  issues++;
1370
1370
  }
1371
1371
 
1372
+ // Hook self-heal runtime: the launcher (scripts/hook-launcher.mjs) degrades a
1373
+ // broken install to exit 0 so it never spams a Node stack trace on every hook
1374
+ // fire. That silence is intentional but hides failure — it drops a breakage
1375
+ // marker so this check can surface the otherwise-invisible degraded state.
1376
+ const brokenMarker = join(MEM_DATA_DIR, 'runtime', 'hook-launcher-broken');
1377
+ if (existsSync(brokenMarker)) {
1378
+ let detail = '';
1379
+ try {
1380
+ const b = JSON.parse(readFileSync(brokenMarker, 'utf8'));
1381
+ const ageH = Math.round((Date.now() - (b.ts || 0)) / 3600000);
1382
+ detail = ` (last: ${b.reason || 'unknown'}, ~${ageH}h ago)`;
1383
+ } catch { /* unreadable marker → bare warning */ }
1384
+ dwarn(`Hook self-heal: a recent hook fire degraded to exit-0${detail} — run \`node ${join(PROJECT_DIR, 'install.mjs')} repair\``);
1385
+ } else {
1386
+ ok('Hook self-heal: no recent silent hook breakage');
1387
+ }
1388
+
1372
1389
  // Plugin/hook lifecycle state
1373
1390
  const settings = readSettings();
1374
1391
  const hasHooks = hasMemHooksConfigured(settings);
@@ -19,31 +19,62 @@
19
19
  // hook dependency graph (no schema.mjs / better-sqlite3 import).
20
20
 
21
21
  import { join } from 'node:path';
22
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
22
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
23
24
 
24
25
  export const NATIVE_BINDING_HINT_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
25
26
  const MARKER_NAME = 'native-binding-hint-last';
26
27
 
28
+ // Resolvable invocation of the bundled CLI's repair path. Absolute via
29
+ // import.meta.url (cli.mjs is one dir up from lib/) so it works on a plugin-only
30
+ // install, where bare `claude-mem-lite` is not on PATH. cli.mjs routes `repair`
31
+ // → install.mjs. (review #3)
32
+ const CLI_REPAIR = `node ${fileURLToPath(new URL('../cli.mjs', import.meta.url))} repair`;
33
+
34
+ // Stable-ish identity of a fault so DISTINCT failures get DISTINCT cooldown
35
+ // windows: the same fault → same key (suppressed within the window), a different
36
+ // fault → different key → surfaces even within the window. djb2 over the message
37
+ // keeps it dependency-free (no node:crypto). (review #8/#15)
38
+ function errKey(message = '') {
39
+ const m = String(message);
40
+ let h = 5381;
41
+ for (let i = 0; i < m.length; i++) h = ((h * 33) ^ m.charCodeAt(i)) >>> 0;
42
+ return h.toString(36);
43
+ }
44
+
27
45
  /**
28
- * True at most once per cooldown window. Persists the last-hint timestamp as
29
- * file CONTENT (not mtime) under runtimeDir so callers can inject `now`
30
- * deterministically in tests. Best-effort: any fs error returns true — showing
31
- * the hint beats silently swallowing a real problem.
46
+ * True at most once per cooldown window FOR A GIVEN `key`. Persists
47
+ * "<epochMs>\t<key>" as file CONTENT (not mtime) under runtimeDir so callers can
48
+ * inject `now` deterministically in tests. A different `key` (a distinct fault)
49
+ * resets the window, so a new problem is never silenced by an earlier, unrelated
50
+ * one. Best-effort: any fs error returns true — showing the hint beats silently
51
+ * swallowing a real problem.
32
52
  *
33
53
  * @param {string} runtimeDir Directory for the marker file
34
54
  * @param {number} [now] Current epoch ms (injectable)
35
55
  * @param {number} [cooldownMs] Suppression window
56
+ * @param {string} [key] Fault identity; same key within the window → suppressed
36
57
  * @returns {boolean}
37
58
  */
38
- export function nativeBindingHintDue(runtimeDir, now = Date.now(), cooldownMs = NATIVE_BINDING_HINT_COOLDOWN_MS) {
59
+ export function nativeBindingHintDue(runtimeDir, now = Date.now(), cooldownMs = NATIVE_BINDING_HINT_COOLDOWN_MS, key = '') {
39
60
  const marker = join(runtimeDir, MARKER_NAME);
40
61
  try {
41
- const last = Number(readFileSync(marker, 'utf8'));
42
- if (Number.isFinite(last) && now - last < cooldownMs) return false;
62
+ const raw = readFileSync(marker, 'utf8');
63
+ // Format "<epochMs>\t<key>"; a legacy bare "<epochMs>" parses with key ''.
64
+ const tab = raw.indexOf('\t');
65
+ const last = Number(tab === -1 ? raw : raw.slice(0, tab));
66
+ const lastKey = tab === -1 ? '' : raw.slice(tab + 1);
67
+ if (Number.isFinite(last) && lastKey === key && now - last < cooldownMs) return false;
43
68
  } catch { /* no/invalid marker → due */ }
44
69
  try {
45
- if (!existsSync(runtimeDir)) mkdirSync(runtimeDir, { recursive: true });
46
- writeFileSync(marker, String(now));
70
+ mkdirSync(runtimeDir, { recursive: true });
71
+ // Atomic write (tmp + rename) so a concurrent reader sees the old or the new
72
+ // COMPLETE value, never a torn timestamp that parses NaN → spurious "due" →
73
+ // duplicate hint. The residual read-then-decide race can still emit twice, but
74
+ // the hint is cosmetic and 6h-rate-limited, so that is acceptable. (#7/#10)
75
+ const tmp = `${marker}.tmp-${process.pid}`;
76
+ writeFileSync(tmp, `${now}\t${key}`);
77
+ renameSync(tmp, marker);
47
78
  } catch { /* best-effort */ }
48
79
  return true;
49
80
  }
@@ -63,9 +94,12 @@ export function nativeBindingHintDue(runtimeDir, now = Date.now(), cooldownMs =
63
94
  export function formatHookError(err, event, { now = Date.now(), runtimeDir } = {}) {
64
95
  const ts = new Date(now).toISOString();
65
96
  if (err && err.code === 'ERR_DLOPEN_FAILED') {
66
- if (runtimeDir && !nativeBindingHintDue(runtimeDir, now)) return null;
97
+ // Key the cooldown on the fault identity so a DISTINCT native failure within
98
+ // the window still surfaces (a second ABI mismatch after a partial rebuild, a
99
+ // corrupt .node) instead of being silenced by a prior, different DLOPEN. (#8/#15)
100
+ if (runtimeDir && !nativeBindingHintDue(runtimeDir, now, NATIVE_BINDING_HINT_COOLDOWN_MS, errKey(err.message))) return null;
67
101
  return `[claude-mem-lite] [${ts}] [WARN] ${event}: native DB binding can't load ` +
68
- `(likely a Node version change) — auto-heals on next MCP server start, or run: claude-mem-lite repair`;
102
+ `(likely a Node version change) — auto-heals on next MCP server start, or run: ${CLI_REPAIR}`;
69
103
  }
70
104
  return `[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err && err.message}`;
71
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "cli.mjs",
28
+ "cli-path.mjs",
28
29
  "mem-cli.mjs",
29
30
  "server.mjs",
30
31
  "server-internals.mjs",
@@ -24,7 +24,7 @@
24
24
  // would defeat the entire purpose — the launcher must survive a broken
25
25
  // install.
26
26
 
27
- import { existsSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
27
+ import { existsSync, mkdirSync, writeFileSync, statSync, unlinkSync, readFileSync } from 'node:fs';
28
28
  import { spawnSync } from 'node:child_process';
29
29
  import { dirname, join } from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -37,8 +37,18 @@ const RUNTIME_DIR = process.env.CLAUDE_MEM_DIR
37
37
  : join(homedir(), '.claude-mem-lite', 'runtime');
38
38
  const HEAL_MARKER = join(RUNTIME_DIR, 'hook-launcher-lastheal');
39
39
  const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
40
+ // Observable breakage state: written when the launcher degrades a broken install
41
+ // to exit 0, cleared once the install is confirmed healthy. `doctor` reads it so
42
+ // the intentional silence (no stack trace per fire) stays detectable. (#4/#8)
43
+ const BROKEN_MARKER = join(RUNTIME_DIR, 'hook-launcher-broken');
40
44
 
41
- // Last-resort recovery string for users whose `claude-mem-lite repair` path
45
+ // Resolvable invocation of the bundled CLI's repair path. Absolute via
46
+ // INSTALL_DIR (import.meta.url) so it works on a plugin-only install, where
47
+ // bare `claude-mem-lite` is not on PATH and ~/.claude-mem-lite/ holds no source.
48
+ // cli.mjs routes `repair` → install.mjs. (review #3)
49
+ const CLI_REPAIR = `node ${join(INSTALL_DIR, 'cli.mjs')} repair`;
50
+
51
+ // Last-resort recovery string for users whose `cli.mjs repair` path
42
52
  // itself failed (install.mjs missing / repair errored / retry still drifting).
43
53
  // Duplicated in install.mjs::repair() catch; both are reachable when local
44
54
  // scripts are broken, so neither can import a shared constant.
@@ -76,13 +86,51 @@ async function runEntry({ bustCache = false } = {}) {
76
86
  // Anchor on any path the error exposes — the missing URL and/or the importer
77
87
  // (present in both messages as "imported from <path>"). If it sits inside our
78
88
  // install, a self-heal could fix it.
89
+ // package.json dependency set of THIS install — read lazily, best-effort. Lets
90
+ // isLocalModuleErr tell a genuinely-ours missing bare dependency (better-sqlite3,
91
+ // zod, …) apart from a foreign/mistyped package name that merely happens to be
92
+ // imported from an install-dir file. The former is self-healable; the latter is
93
+ // a real packaging bug that must surface a Node stack trace rather than be
94
+ // swallowed by an exit-0 self-heal. (review #5/#7)
95
+ function ownDependencies() {
96
+ try {
97
+ const pkg = JSON.parse(readFileSync(join(INSTALL_DIR, 'package.json'), 'utf8'));
98
+ return new Set([
99
+ ...Object.keys(pkg.dependencies || {}),
100
+ ...Object.keys(pkg.optionalDependencies || {}),
101
+ ]);
102
+ } catch {
103
+ return null; // unreadable package.json → caller stays permissive
104
+ }
105
+ }
106
+
79
107
  function isLocalModuleErr(e) {
80
108
  if (!e || e.code !== 'ERR_MODULE_NOT_FOUND') return false;
81
- const importer = /imported from (.+?)\s*$/.exec(String(e.message || ''))?.[1];
82
- const paths = [e.url, importer]
83
- .filter(Boolean)
84
- .map((p) => String(p).replace(/^file:\/\//, ''));
85
- return paths.some((p) => p.startsWith(INSTALL_DIR) || p.includes('.claude-mem-lite'));
109
+ // Missing RELATIVE module: e.url is the missing file's URL. Ours iff it sits
110
+ // under our install dir (the `.claude-mem-lite` substring also covers the
111
+ // symlink-farm dev/direct-install case where INSTALL_DIR is the realpath).
112
+ if (e.url) {
113
+ const p = String(e.url).replace(/^file:\/\//, '');
114
+ return p.startsWith(INSTALL_DIR) || p.includes('.claude-mem-lite');
115
+ }
116
+ // Missing BARE dependency: e.url is UNDEFINED; message is
117
+ // `Cannot find package '<name>' imported from <importer>`. The (.+) capture
118
+ // needs no `m` flag (`.` stops at a newline) and so tolerates a multi-line
119
+ // message that appends a hint line after the importer path — the old
120
+ // `(.+?)\s*$` returned undefined there and misclassified the dep. (review #11/#15)
121
+ const msg = String(e.message || '');
122
+ const importer = /imported from (.+)/.exec(msg)?.[1]?.trim();
123
+ if (!importer) return false;
124
+ const importerPath = importer.replace(/^file:\/\//, '');
125
+ if (!(importerPath.startsWith(INSTALL_DIR) || importerPath.includes('.claude-mem-lite'))) return false;
126
+ // Importer is ours — but only self-heal if the missing package is one we
127
+ // actually declare. A foreign/typo'd name re-throws so the bug is visible.
128
+ const pkgName = /Cannot find package '([^']+)'/.exec(msg)?.[1];
129
+ const deps = ownDependencies();
130
+ if (!deps || !pkgName) return true; // best-effort: can't verify → stay permissive
131
+ // Normalize sub-path / scoped imports to the package root (better-sqlite3/x → better-sqlite3).
132
+ const root = pkgName.startsWith('@') ? pkgName.split('/').slice(0, 2).join('/') : pkgName.split('/')[0];
133
+ return deps.has(root);
86
134
  }
87
135
 
88
136
  // Human-readable label for the "Detected broken install (<reason>)" line:
@@ -107,11 +155,29 @@ function recordHealAttempt() {
107
155
  } catch { /* best-effort */ }
108
156
  }
109
157
 
158
+ // Drop the 6h cooldown once a heal fully resolves. The marker is written BEFORE
159
+ // spawn (rate-limits concurrent fires), but a SUCCESSFUL heal must not keep
160
+ // blocking an unrelated later breakage that happens within the window. (#6/#9)
161
+ function clearHealMarker() {
162
+ try { unlinkSync(HEAL_MARKER); } catch { /* already gone — fine */ }
163
+ }
164
+
165
+ function recordBreakage(reason) {
166
+ try {
167
+ mkdirSync(RUNTIME_DIR, { recursive: true });
168
+ writeFileSync(BROKEN_MARKER, JSON.stringify({ reason, ts: Date.now() }));
169
+ } catch { /* best-effort */ }
170
+ }
171
+
172
+ function clearBreakage() {
173
+ try { if (existsSync(BROKEN_MARKER)) unlinkSync(BROKEN_MARKER); } catch { /* best-effort */ }
174
+ }
175
+
110
176
  async function attemptHeal(reason) {
111
177
  if (recentHealAttempt()) {
112
178
  process.stderr.write(
113
179
  `[claude-mem-lite] Self-heal skipped (last attempt < 6h ago).\n` +
114
- `[claude-mem-lite] Manual recovery: claude-mem-lite repair\n` +
180
+ `[claude-mem-lite] Manual recovery: ${CLI_REPAIR}\n` +
115
181
  `[claude-mem-lite] If that fails, run: ${TARBALL_FALLBACK}\n`,
116
182
  );
117
183
  return false;
@@ -160,19 +226,30 @@ if (rest.includes('session-start')) {
160
226
 
161
227
  try {
162
228
  await runEntry();
229
+ // A clean session-start fire confirms the install is healthy → clear any stale
230
+ // breakage marker. Gated to session-start so the per-tool hot path pays nothing.
231
+ if (rest.includes('session-start')) clearBreakage();
163
232
  } catch (e) {
164
233
  if (!isLocalModuleErr(e)) throw e;
165
- const healed = await attemptHeal(describeFailure(e));
234
+ const reason = describeFailure(e);
235
+ const healed = await attemptHeal(reason);
166
236
  if (!healed) {
167
237
  // Broken/missing dependency we can't repair right now (repair failed, or
168
238
  // was skipped within the 6h cooldown). attemptHeal already wrote actionable
169
239
  // guidance — degrade quietly instead of re-throwing the original import
170
- // error, which would spew a Node stack trace on every hook fire.
240
+ // error, which would spew a Node stack trace on every hook fire. Record the
241
+ // breakage so the exit-0 silence stays observable to `doctor`. (#4/#8)
242
+ recordBreakage(reason);
171
243
  process.exit(0);
172
244
  }
173
245
  try {
174
246
  await runEntry({ bustCache: true });
247
+ // Fully healed: drop the cooldown so an UNRELATED later break can heal
248
+ // immediately (#6/#9), and clear the breakage marker.
249
+ clearHealMarker();
250
+ clearBreakage();
175
251
  } catch (retryErr) {
252
+ recordBreakage(`retry-failed: ${retryErr.message}`);
176
253
  process.stderr.write(
177
254
  `[claude-mem-lite] Hook still failing after self-heal: ${retryErr.message}\n` +
178
255
  `[claude-mem-lite] Manual recovery: ${TARBALL_FALLBACK}\n`,
@@ -4,6 +4,7 @@
4
4
  import { debugCatch, COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, OBS_BM25 } from './utils.mjs';
5
5
  import { BASE_STOP_WORDS } from './stop-words.mjs';
6
6
  import { porterStem } from './tfidf.mjs';
7
+ import { CLI_INVOKE } from './cli-path.mjs';
7
8
 
8
9
  // ─── MCP Server Instructions Builder ───────────────────────────────────────
9
10
  // Phase A (v2.31.3+): when quiet=true, drops WHEN-TO-USE proactive-trigger and
@@ -14,13 +15,13 @@ import { porterStem } from './tfidf.mjs';
14
15
  const INSTRUCTIONS_BASE = [
15
16
  'Long-term memory across sessions. Hooks auto-inject context; CLI preferred for explicit queries.',
16
17
  '',
17
- 'CLI (via Bash) — invoke as `node ~/.claude-mem-lite/cli.mjs <cmd>` (shipped with the plugin), or bare `claude-mem-lite <cmd>` only if you ran the optional global `npm i -g claude-mem-lite`:',
18
- ' claude-mem-lite search "query" — FTS5 full-text search',
19
- ' claude-mem-lite search "err" --type bugfix — filter by type',
20
- ' claude-mem-lite recall "file.mjs" — file-related memories',
21
- ' claude-mem-lite recent 5 — latest observations',
22
- ' claude-mem-lite get 42,43 — full details by ID',
23
- ' claude-mem-lite timeline --anchor 42 — chronological context',
18
+ `CLI (via Bash) — invoke as \`${CLI_INVOKE} <cmd>\` (resolves on any install shape; the bare \`claude-mem-lite\` shorthand works only after an optional global \`npm i -g claude-mem-lite\`):`,
19
+ ` ${CLI_INVOKE} search "query" — FTS5 full-text search`,
20
+ ` ${CLI_INVOKE} search "err" --type bugfix — filter by type`,
21
+ ` ${CLI_INVOKE} recall "file.mjs" — file-related memories`,
22
+ ` ${CLI_INVOKE} recent 5 — latest observations`,
23
+ ` ${CLI_INVOKE} get 42,43 — full details by ID`,
24
+ ` ${CLI_INVOKE} timeline --anchor 42 — chronological context`,
24
25
  '',
25
26
  'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline for programmatic access (always available — no PATH/CLI install needed).',
26
27
  'mem_save: Save non-obvious insights (bugfix lessons, architecture decisions).',
package/source-files.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  export const SOURCE_FILES = [
8
8
  // Entry points and top-level modules
9
- 'cli.mjs', 'server.mjs', 'server-internals.mjs', 'search-engine.mjs', 'tool-schemas.mjs',
9
+ 'cli.mjs', 'cli-path.mjs', 'server.mjs', 'server-internals.mjs', 'search-engine.mjs', 'tool-schemas.mjs',
10
10
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
11
11
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs',
12
12
  'hook-update.mjs', 'hook-optimize.mjs', 'hook-precompact.mjs',
package/tool-schemas.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  // Single source of truth — used by server.mjs (runtime) and contract.test.mjs (validation tests)
3
3
 
4
4
  import { z } from 'zod';
5
+ import { CLI_INVOKE } from './cli-path.mjs';
5
6
 
6
7
  export const OBS_TYPE_ENUM = z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
7
8
 
@@ -349,7 +350,7 @@ export const tools = [
349
350
  ' - Looking for prior art on a module/feature before refactoring\n' +
350
351
  ' - User asks "have we seen this before" or references something not in visible context\n' +
351
352
  '\n' +
352
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs search "<query>" [--type bugfix]',
353
+ 'Equivalent CLI: ' + CLI_INVOKE + ' search "<query>" [--type bugfix]',
353
354
  inputSchema: memSearchSchema,
354
355
  },
355
356
  {
@@ -367,7 +368,7 @@ export const tools = [
367
368
  ' - User asks "what did we do yesterday / last" with no topic keyword\n' +
368
369
  ' - Verifying that a just-made change was captured as an observation\n' +
369
370
  '\n' +
370
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs recent [N]',
371
+ 'Equivalent CLI: ' + CLI_INVOKE + ' recent [N]',
371
372
  inputSchema: memRecentSchema,
372
373
  },
373
374
  {
@@ -387,7 +388,7 @@ export const tools = [
387
388
  ' - A search hit is interesting and you want its chronological neighbours\n' +
388
389
  ' - Replaying a session narrative around a known observation ID\n' +
389
390
  '\n' +
390
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs timeline --anchor <ID> [--before N --after N]',
391
+ 'Equivalent CLI: ' + CLI_INVOKE + ' timeline --anchor <ID> [--before N --after N]',
391
392
  inputSchema: memTimelineSchema,
392
393
  },
393
394
  {
@@ -407,7 +408,7 @@ export const tools = [
407
408
  '\n' +
408
409
  'On miss, response includes "Try: …" hint listing other sources the ID lives in.\n' +
409
410
  '\n' +
410
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs get <id>[,<id>,...] — accepts P#/S#/# prefix.',
411
+ 'Equivalent CLI: ' + CLI_INVOKE + ' get <id>[,<id>,...] — accepts P#/S#/# prefix.',
411
412
  inputSchema: memGetSchema,
412
413
  },
413
414
  {
@@ -425,7 +426,7 @@ export const tools = [
425
426
  ' - Cleaning up an observation saved from a test run or incorrect save\n' +
426
427
  ' - Always run once with confirm=false, then again with confirm=true\n' +
427
428
  '\n' +
428
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs delete <id>[,<id>,...] [--confirm]',
429
+ 'Equivalent CLI: ' + CLI_INVOKE + ' delete <id>[,<id>,...] [--confirm]',
429
430
  inputSchema: memDeleteSchema,
430
431
  hidden: true,
431
432
  },
@@ -444,7 +445,7 @@ export const tools = [
444
445
  ' - After a non-obvious architecture/tradeoff decision — set type="decision", lesson_learned="<constraint + why>"\n' +
445
446
  ' - User explicitly asks "remember this" or "save a note that ..."\n' +
446
447
  '\n' +
447
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs save --type bugfix --lesson "..." "<content>"',
448
+ 'Equivalent CLI: ' + CLI_INVOKE + ' save --type bugfix --lesson "..." "<content>"',
448
449
  inputSchema: memSaveSchema,
449
450
  },
450
451
  {
@@ -462,7 +463,7 @@ export const tools = [
462
463
  ' - Diagnosing why search feels sparse or noisy at a macro level\n' +
463
464
  ' - Auditing a project before major compression/maintenance\n' +
464
465
  '\n' +
465
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs stats [--project X] [--days 30]',
466
+ 'Equivalent CLI: ' + CLI_INVOKE + ' stats [--project X] [--days 30]',
466
467
  inputSchema: memStatsSchema,
467
468
  hidden: true,
468
469
  },
@@ -481,7 +482,7 @@ export const tools = [
481
482
  ' - After a major project phase completes and old per-file observations are noise\n' +
482
483
  ' - Stats show thousands of low-importance rows dragging search quality\n' +
483
484
  '\n' +
484
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs compress [--execute] [--age-days 90] (preview is default)',
485
+ 'Equivalent CLI: ' + CLI_INVOKE + ' compress [--execute] [--age-days 90] (preview is default)',
485
486
  inputSchema: memCompressSchema,
486
487
  hidden: true,
487
488
  },
@@ -500,7 +501,7 @@ export const tools = [
500
501
  ' - After bulk imports or a long offline period\n' +
501
502
  ' - User asks for periodic maintenance / cleanup\n' +
502
503
  '\n' +
503
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs maintain scan --ops dedup,decay',
504
+ 'Equivalent CLI: ' + CLI_INVOKE + ' maintain scan --ops dedup,decay',
504
505
  inputSchema: memMaintainSchema,
505
506
  hidden: true,
506
507
  },
@@ -519,7 +520,7 @@ export const tools = [
519
520
  ' - stats show many degraded (title-only, no lesson) observations\n' +
520
521
  ' - Start with action="preview" to see candidates before spending tokens\n' +
521
522
  '\n' +
522
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs optimize [--run|--run-all] [--task re-enrich,normalize,cluster-merge,smart-compress] [--max N] (preview is default)',
523
+ 'Equivalent CLI: ' + CLI_INVOKE + ' optimize [--run|--run-all] [--task re-enrich,normalize,cluster-merge,smart-compress] [--max N] (preview is default)',
523
524
  inputSchema: memOptimizeSchema,
524
525
  hidden: true,
525
526
  },
@@ -538,7 +539,7 @@ export const tools = [
538
539
  ' - Looking for a tool by capability → action="search" with keywords\n' +
539
540
  ' - User explicitly asks to import a GitHub repo → action="import_url"\n' +
540
541
  '\n' +
541
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs registry <list|search|import|...> [args]',
542
+ 'Equivalent CLI: ' + CLI_INVOKE + ' registry <list|search|import|...> [args]',
542
543
  inputSchema: memRegistrySchema,
543
544
  hidden: true,
544
545
  },
@@ -576,7 +577,7 @@ export const tools = [
576
577
  ' - You later discover additional context worth appending to lesson_learned\n' +
577
578
  ' - Reclassifying an observation after its true type becomes clear\n' +
578
579
  '\n' +
579
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs update <id> [--title ...] [--lesson ...]',
580
+ 'Equivalent CLI: ' + CLI_INVOKE + ' update <id> [--title ...] [--lesson ...]',
580
581
  inputSchema: memUpdateSchema,
581
582
  hidden: true,
582
583
  },
@@ -595,7 +596,7 @@ export const tools = [
595
596
  ' - Moving observations between machines or projects\n' +
596
597
  ' - User asks for a JSON snapshot of a project\'s memories\n' +
597
598
  '\n' +
598
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs export [--format jsonl] [--project X] [--limit 500]',
599
+ 'Equivalent CLI: ' + CLI_INVOKE + ' export [--format jsonl] [--project X] [--limit 500]',
599
600
  inputSchema: memExportSchema,
600
601
  hidden: true,
601
602
  },
@@ -614,7 +615,7 @@ export const tools = [
614
615
  ' - User asks "what do we know about <file>"\n' +
615
616
  ' - Investigating a recurring issue in a file you have not touched recently\n' +
616
617
  '\n' +
617
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs recall "<file>" [--limit 10]',
618
+ 'Equivalent CLI: ' + CLI_INVOKE + ' recall "<file>" [--limit 10]',
618
619
  inputSchema: memRecallSchema,
619
620
  },
620
621
  {
@@ -632,7 +633,7 @@ export const tools = [
632
633
  ' - After a crash, power loss, or manual DB edit\n' +
633
634
  ' - doctor / stats flags FTS integrity problems\n' +
634
635
  '\n' +
635
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs fts-check [--rebuild]',
636
+ 'Equivalent CLI: ' + CLI_INVOKE + ' fts-check [--rebuild]',
636
637
  inputSchema: memFtsCheckSchema,
637
638
  hidden: true,
638
639
  },
@@ -651,7 +652,7 @@ export const tools = [
651
652
  ' - Triaging what to compress or clean up before running maintenance\n' +
652
653
  ' - Scanning for interesting anchors to follow up with mem_timeline\n' +
653
654
  '\n' +
654
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs browse [--tier active] [--project X]',
655
+ 'Equivalent CLI: ' + CLI_INVOKE + ' browse [--tier active] [--project X]',
655
656
  inputSchema: memBrowseSchema,
656
657
  hidden: true,
657
658
  },
@@ -670,7 +671,7 @@ export const tools = [
670
671
  ' - Wrap-up phase enumerates follow-up items for the next session\n' +
671
672
  ' - Bug surfaces but root cause is out of this session\'s scope\n' +
672
673
  '\n' +
673
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
674
+ 'Equivalent CLI: ' + CLI_INVOKE + ' defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
674
675
  inputSchema: memDeferSchema,
675
676
  },
676
677
  {
@@ -687,7 +688,7 @@ export const tools = [
687
688
  ' - About to refer to "item N" and need to confirm what N points to\n' +
688
689
  ' - Auditing carry-forward state across multiple sessions\n' +
689
690
  '\n' +
690
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs defer list [--project X] [--limit 10]',
691
+ 'Equivalent CLI: ' + CLI_INVOKE + ' defer list [--project X] [--limit 10]',
691
692
  inputSchema: memDeferListSchema,
692
693
  },
693
694
  {
@@ -705,7 +706,7 @@ export const tools = [
705
706
  ' - Scope changed and the work is no longer needed\n' +
706
707
  ' - User explicitly says "drop the deferred X, never mind"\n' +
707
708
  '\n' +
708
- 'Equivalent CLI: node ~/.claude-mem-lite/cli.mjs defer drop <D#N|ordinal> --reason "..."',
709
+ 'Equivalent CLI: ' + CLI_INVOKE + ' defer drop <D#N|ordinal> --reason "..."',
709
710
  inputSchema: memDeferDropSchema,
710
711
  },
711
712
  ];