claude-mem-lite 3.0.1 → 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.0.1",
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.0.1",
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,6 +160,8 @@ 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 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
+
163
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.
164
166
 
165
167
  ### Method 2: npx (one-liner)
@@ -261,9 +263,9 @@ surface — reach them through the CLI column in the second table.
261
263
  | `mem_update` | `claude-mem-lite update <id>` | Edit an observation in place. |
262
264
  | `mem_stats` | `claude-mem-lite stats` | Counts, type distribution, daily activity. |
263
265
  | `mem_delete` | `claude-mem-lite delete <id>` | Preview / confirm workflow, FTS5 cleanup. |
264
- | `mem_compress` | `claude-mem-lite compress --preview` | Roll up old low-value observations. |
265
- | `mem_maintain` | `claude-mem-lite maintain --action scan` | dedup / decay / cleanup / rebuild_vectors. |
266
- | `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). |
267
269
  | `mem_export` | `claude-mem-lite export` | JSON / JSONL dump, filters by project, type, date. |
268
270
  | `mem_fts_check` | `claude-mem-lite fts-check [--rebuild]` | FTS5 integrity + rebuild. |
269
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
- > 由 \`claude-mem-lite adopt\` 生成;卸载用 \`claude-mem-lite 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
- | 清理过期记忆 | \`claude-mem-lite maintain --action scan\` → \`--action execute\` |
62
- | 深度优化(Haiku) | \`claude-mem-lite optimize --action preview\` |
63
- | 压缩旧条目 | \`claude-mem-lite compress --preview\` |
64
- | FTS5 索引检查 / 重建 | \`claude-mem-lite fts-check [--rebuild]\` |
65
- | tier 分组浏览 | \`claude-mem-lite browse [--tier active]\` |
66
- | 导出 JSON/JSONL | \`claude-mem-lite export [--format jsonl]\` |
67
- | 统计总量 / 健康 | \`claude-mem-lite stats [--days 30]\` |
68
- | 删除某条 | \`claude-mem-lite delete <id>[,<id>]\` |
69
- | 更新某条 | \`claude-mem-lite update <id> [--title ...]\` |
70
- | 列 / 搜索 / 导入 skill-agent registry | \`claude-mem-lite 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
- | \`claude-mem-lite search "query"\` | FTS5 全文搜索 |
78
- | \`claude-mem-lite search "err" --type bugfix\` | 按类型过滤 |
79
- | \`claude-mem-lite recall "file.mjs"\` | 文件相关记忆 |
80
- | \`claude-mem-lite recent 5\` | 最近 5 条 |
81
- | \`claude-mem-lite get 42,43\` | 按 ID 展开 |
82
- | \`claude-mem-lite 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
- \`claude-mem-lite 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
- !claude-mem-lite 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
- claude-mem-lite 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
- claude-mem-lite 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 `claude-mem-lite search <query>` via Bash
27
- - `/mem recent` or `/mem recent 20` → run `claude-mem-lite recent [N]` via Bash
28
- - `/mem recall <file>` → run `claude-mem-lite recall <file>` via Bash
29
- - `/mem timeline <id>` → run `claude-mem-lite 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 `claude-mem-lite stats` via Bash
32
- - `/mem get <ids>` → run `claude-mem-lite 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 `claude-mem-lite 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 `claude-mem-lite 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
- !claude-mem-lite unadopt $ARGUMENTS
30
+ !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS
package/hook.mjs CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  } from './hook-shared.mjs';
46
46
  import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
47
47
  import { scrubRecord } from './lib/scrub-record.mjs';
48
+ import { formatHookError } from './lib/native-binding-hint.mjs';
48
49
  import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
49
50
  import { cleanupBroken, decayAndMarkIdle, boostAccessed } from './lib/maintain-core.mjs';
50
51
  import {
@@ -1439,9 +1440,13 @@ try {
1439
1440
  case 'update-check': await checkForUpdate(); break;
1440
1441
  }
1441
1442
  } catch (err) {
1442
- // Always log fatal errors (ungated) with structured format
1443
- const ts = new Date().toISOString();
1444
- console.error(`[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err.message}`);
1443
+ // Log fatal errors (ungated) with structured format. ERR_DLOPEN_FAILED (an
1444
+ // unloadable native DB binding, e.g. ABI-stale after a Node upgrade) is
1445
+ // collapsed to one short, rate-limited rebuild hint instead of the raw
1446
+ // multi-line NODE_MODULE_VERSION message on every fire — see
1447
+ // lib/native-binding-hint.mjs.
1448
+ const line = formatHookError(err, event, { runtimeDir: RUNTIME_DIR });
1449
+ if (line) console.error(line);
1445
1450
  }
1446
1451
 
1447
1452
  process.exit(0);
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);
@@ -0,0 +1,105 @@
1
+ // lib/native-binding-hint.mjs — friendly, rate-limited hint for an unloadable
2
+ // native DB binding (better-sqlite3 ERR_DLOPEN_FAILED, e.g. a Node version
3
+ // upgrade leaves the prebuilt .node ABI-stale).
4
+ //
5
+ // This is the SIBLING of the missing-dependency case handled in
6
+ // scripts/hook-launcher.mjs. The two fail on different paths:
7
+ // • MISSING dependency (ERR_MODULE_NOT_FOUND) throws at IMPORT time, before
8
+ // hook.mjs runs — caught by the launcher.
9
+ // • UNLOADABLE binding (ERR_DLOPEN_FAILED) imports fine (better-sqlite3 loads
10
+ // its .node lazily at the first `new Database()`), then throws inside a hook
11
+ // handler — caught by hook.mjs's top-level dispatch try/catch. Pre-this,
12
+ // that catch logged the raw multi-line NODE_MODULE_VERSION message on EVERY
13
+ // hook fire. Here we collapse it to one short, actionable line, rate-limited
14
+ // per cooldown. The actual rebuild is the MCP server launch path's job
15
+ // (lib/binding-probe.mjs::ensureBetterSqlite3Working) — a hook must never
16
+ // run `npm rebuild` itself (2–5s timeout + concurrent-fire races).
17
+ //
18
+ // Pure node: imports + injectable now/runtimeDir so it unit-tests without the
19
+ // hook dependency graph (no schema.mjs / better-sqlite3 import).
20
+
21
+ import { join } from 'node:path';
22
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ export const NATIVE_BINDING_HINT_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
26
+ const MARKER_NAME = 'native-binding-hint-last';
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
+
45
+ /**
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.
52
+ *
53
+ * @param {string} runtimeDir Directory for the marker file
54
+ * @param {number} [now] Current epoch ms (injectable)
55
+ * @param {number} [cooldownMs] Suppression window
56
+ * @param {string} [key] Fault identity; same key within the window → suppressed
57
+ * @returns {boolean}
58
+ */
59
+ export function nativeBindingHintDue(runtimeDir, now = Date.now(), cooldownMs = NATIVE_BINDING_HINT_COOLDOWN_MS, key = '') {
60
+ const marker = join(runtimeDir, MARKER_NAME);
61
+ try {
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;
68
+ } catch { /* no/invalid marker → due */ }
69
+ try {
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);
78
+ } catch { /* best-effort */ }
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Single stderr line hook.mjs should log for a caught dispatch error, or null
84
+ * to stay silent (ERR_DLOPEN_FAILED still within cooldown). ERR_DLOPEN_FAILED →
85
+ * short rate-limited rebuild hint; everything else → the existing ungated
86
+ * structured ERROR line. Pass runtimeDir to enable rate-limiting (omit it to
87
+ * always format, e.g. in tests).
88
+ *
89
+ * @param {Error & {code?: string}} err
90
+ * @param {string} event Hook event name (stop / session-start / …)
91
+ * @param {{now?: number, runtimeDir?: string}} [opts]
92
+ * @returns {string | null}
93
+ */
94
+ export function formatHookError(err, event, { now = Date.now(), runtimeDir } = {}) {
95
+ const ts = new Date(now).toISOString();
96
+ if (err && err.code === 'ERR_DLOPEN_FAILED') {
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;
101
+ return `[claude-mem-lite] [${ts}] [WARN] ${event}: native DB binding can't load ` +
102
+ `(likely a Node version change) — auto-heals on next MCP server start, or run: ${CLI_REPAIR}`;
103
+ }
104
+ return `[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err && err.message}`;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.0.1",
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",
@@ -79,6 +80,7 @@
79
80
  "lib/deferred-work.mjs",
80
81
  "lib/upgrade-banner.mjs",
81
82
  "lib/scrub-record.mjs",
83
+ "lib/native-binding-hint.mjs",
82
84
  "lib/import-jsonl.mjs",
83
85
  "cli/common.mjs",
84
86
  "cli/fts-check.mjs",
@@ -9,16 +9,22 @@
9
9
  // launcher is defense-in-depth for similar future drift (corrupt download,
10
10
  // half-applied install, manual file deletion).
11
11
  //
12
- // Behavior: try-import the target entry. On ERR_MODULE_NOT_FOUND whose URL
13
- // points under the install dir, run `install.mjs repair` (rate-limited via a
14
- // 6h marker file under runtime/) and retry the import once. On any other
15
- // exception, re-throw so Node's default error surface is preserved.
12
+ // Behavior: try-import the target entry. On ERR_MODULE_NOT_FOUND originating
13
+ // from our install either a missing relative module (e.url under the install
14
+ // dir) or a missing bare dependency like better-sqlite3 (e.url is undefined and
15
+ // the importer named in the message is under the install dir) — run
16
+ // `install.mjs repair` (rate-limited via a 6h marker file under runtime/) and
17
+ // retry the import once. If repair is unavailable or fails, degrade quietly:
18
+ // these are best-effort memory hooks, so a broken/missing dependency emits one
19
+ // clean recovery line and exits 0 rather than dumping a Node stack trace on
20
+ // every fire. On any other (foreign) exception, re-throw so Node's default
21
+ // error surface is preserved.
16
22
  //
17
23
  // HARD constraint: pure node: imports only. Importing anything from lib/ here
18
24
  // would defeat the entire purpose — the launcher must survive a broken
19
25
  // install.
20
26
 
21
- import { existsSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
27
+ import { existsSync, mkdirSync, writeFileSync, statSync, unlinkSync, readFileSync } from 'node:fs';
22
28
  import { spawnSync } from 'node:child_process';
23
29
  import { dirname, join } from 'node:path';
24
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -31,8 +37,18 @@ const RUNTIME_DIR = process.env.CLAUDE_MEM_DIR
31
37
  : join(homedir(), '.claude-mem-lite', 'runtime');
32
38
  const HEAL_MARKER = join(RUNTIME_DIR, 'hook-launcher-lastheal');
33
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');
34
44
 
35
- // 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
36
52
  // itself failed (install.mjs missing / repair errored / retry still drifting).
37
53
  // Duplicated in install.mjs::repair() catch; both are reachable when local
38
54
  // scripts are broken, so neither can import a shared constant.
@@ -59,10 +75,71 @@ async function runEntry({ bustCache = false } = {}) {
59
75
  await import(url);
60
76
  }
61
77
 
78
+ // Two ERR_MODULE_NOT_FOUND shapes reach here (both verified against Node 22):
79
+ // • missing relative module → e.url = file://<missing-path> (under install)
80
+ // • missing bare dependency (e.g. a half-installed better-sqlite3) → e.url is
81
+ // UNDEFINED and the message is `Cannot find package '<name>' imported from
82
+ // <importer>`. This is the shape that bricked the hooks: the old
83
+ // file://INSTALL_DIR prefix test never matched it in a dev-dir install, so
84
+ // a missing dependency was misread as a foreign error and re-thrown as a
85
+ // Node stack trace on every hook fire.
86
+ // Anchor on any path the error exposes — the missing URL and/or the importer
87
+ // (present in both messages as "imported from <path>"). If it sits inside our
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
+
62
107
  function isLocalModuleErr(e) {
63
108
  if (!e || e.code !== 'ERR_MODULE_NOT_FOUND') return false;
64
- const where = String(e.url || e.message || '');
65
- return where.includes('.claude-mem-lite') || where.startsWith(`file://${INSTALL_DIR}`);
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);
134
+ }
135
+
136
+ // Human-readable label for the "Detected broken install (<reason>)" line:
137
+ // prefer the missing dependency/module name over a raw path fragment.
138
+ function describeFailure(e) {
139
+ const pkg = /Cannot find package '([^']+)'/.exec(String(e.message || ''))?.[1];
140
+ if (pkg) return pkg;
141
+ if (e.url) return String(e.url).split('/').pop();
142
+ return String(e.message || 'unknown').split('/').slice(-2).join('/');
66
143
  }
67
144
 
68
145
  function recentHealAttempt() {
@@ -78,11 +155,29 @@ function recordHealAttempt() {
78
155
  } catch { /* best-effort */ }
79
156
  }
80
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
+
81
176
  async function attemptHeal(reason) {
82
177
  if (recentHealAttempt()) {
83
178
  process.stderr.write(
84
179
  `[claude-mem-lite] Self-heal skipped (last attempt < 6h ago).\n` +
85
- `[claude-mem-lite] Manual recovery: claude-mem-lite repair\n` +
180
+ `[claude-mem-lite] Manual recovery: ${CLI_REPAIR}\n` +
86
181
  `[claude-mem-lite] If that fails, run: ${TARBALL_FALLBACK}\n`,
87
182
  );
88
183
  return false;
@@ -131,18 +226,34 @@ if (rest.includes('session-start')) {
131
226
 
132
227
  try {
133
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();
134
232
  } catch (e) {
135
233
  if (!isLocalModuleErr(e)) throw e;
136
- const reason = String(e.url || e.message).split('/').slice(-2).join('/');
234
+ const reason = describeFailure(e);
137
235
  const healed = await attemptHeal(reason);
138
- if (!healed) throw e;
236
+ if (!healed) {
237
+ // Broken/missing dependency we can't repair right now (repair failed, or
238
+ // was skipped within the 6h cooldown). attemptHeal already wrote actionable
239
+ // guidance — degrade quietly instead of re-throwing the original import
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);
243
+ process.exit(0);
244
+ }
139
245
  try {
140
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();
141
251
  } catch (retryErr) {
252
+ recordBreakage(`retry-failed: ${retryErr.message}`);
142
253
  process.stderr.write(
143
254
  `[claude-mem-lite] Hook still failing after self-heal: ${retryErr.message}\n` +
144
255
  `[claude-mem-lite] Manual recovery: ${TARBALL_FALLBACK}\n`,
145
256
  );
146
- process.exit(1);
257
+ process.exit(0);
147
258
  }
148
259
  }
@@ -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,15 +15,15 @@ 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):',
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
- 'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline for programmatic access.',
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).',
27
28
  'Search tips: short keywords (2-3 words), filter with obs_type when relevant.',
28
29
  ];
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',
@@ -121,6 +121,10 @@ export const SOURCE_FILES = [
121
121
  // Statically imported by hook-llm, hook-handoff, hook-optimize, hook,
122
122
  // mem-cli; reached transitively from server.mjs and cli.mjs.
123
123
  'lib/scrub-record.mjs',
124
+ // Rate-limited friendly hint for an unloadable native DB binding
125
+ // (ERR_DLOPEN_FAILED). Statically imported by hook.mjs; ship it so the
126
+ // dispatch catch path resolves in installed/tarball runtimes.
127
+ 'lib/native-binding-hint.mjs',
124
128
  // Cold-start backfill: parses ~/.claude/projects/<encoded>/<uuid>.jsonl
125
129
  // transcripts into user_prompts + observations. Dynamic-imported by
126
130
  // mem-cli.mjs::cmdImportJsonl; listed here so source-files-sync.test.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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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: claude-mem-lite 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
  ];