claude-mem-lite 6.29.2 → 6.30.0

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +9 -11
  4. package/README.zh-CN.md +18 -14
  5. package/adopt-cli.mjs +56 -20
  6. package/adopt-content.mjs +1 -2
  7. package/claudemd.mjs +31 -5
  8. package/cli/activity.mjs +3 -2
  9. package/cli/common.mjs +35 -4
  10. package/cli/fts-check.mjs +2 -1
  11. package/cli/verify-apply.mjs +4 -4
  12. package/cli.mjs +5 -1
  13. package/commands/adopt.md +7 -7
  14. package/commands/bug.md +3 -3
  15. package/commands/lesson.md +3 -3
  16. package/commands/mem.md +2 -2
  17. package/commands/unadopt.md +2 -2
  18. package/commands/update.md +2 -2
  19. package/commands/verify.md +2 -4
  20. package/haiku-client.mjs +6 -1
  21. package/hook-context.mjs +2 -2
  22. package/hook-llm.mjs +22 -16
  23. package/hook-optimize.mjs +180 -176
  24. package/hook-shared.mjs +7 -0
  25. package/hook.mjs +18 -11
  26. package/install.mjs +12 -10
  27. package/lib/activity.mjs +1 -1
  28. package/lib/db-unusable.mjs +43 -12
  29. package/lib/edge-attribution.mjs +1 -1
  30. package/lib/get-core.mjs +3 -3
  31. package/lib/local-steering.mjs +14 -3
  32. package/lib/maintain-core.mjs +35 -1
  33. package/lib/project-rekey.mjs +6 -2
  34. package/lib/quiet-scope.mjs +6 -2
  35. package/lib/save-nudge.mjs +6 -3
  36. package/lib/schema-skew.mjs +4 -2
  37. package/lib/startup-dashboard.mjs +3 -2
  38. package/lib/stats-quality.mjs +6 -4
  39. package/lib/upgrade-banner.mjs +2 -1
  40. package/mem-cli.mjs +58 -36
  41. package/memdir.mjs +1 -1
  42. package/npm-shrinkwrap.json +5 -5
  43. package/package.json +1 -1
  44. package/search-scoring.mjs +6 -6
  45. package/secret-scrub.mjs +4 -1
  46. package/server.mjs +24 -9
  47. package/tool-schemas.mjs +2 -2
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.29.2",
12
+ "version": "6.30.0",
13
13
  "source": "./",
14
14
  "homepage": "https://github.com/sdsrss/claude-mem-lite",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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)."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.29.2",
3
+ "version": "6.30.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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
@@ -124,7 +124,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
124
124
  <br>Original behaviour and the measurements behind it via explicit keywords or FTS5 term overlap. **The `/clear` and `/compact` arm fires since v5.4.0** (R10-P1-1); before that it had never once written a row — `session_handoffs` on the maintainer's install held 4 `exit` rows and **0** `clear` rows. Two host facts settled it, both measured rather than assumed. (1) `Stop` runs at the end of every assistant *turn*, not once per session, and it deleted the session file that SessionStart reads to learn which session just ended — so the branch was unreachable, and mem sessions were minted per turn (58 prompts over 16 host sessions produced 56 mem sessions and 56 summary rows, 2026-09-07). (2) Claude Code **rotates its session id across `/clear`**: of 21 real transcripts, 12 carry a `/clear` command record, and in 12/12 that record's timestamp precedes its own file's first record by ~0.1s — the command is issued in the old session and replayed into a new file under a new id. So `Stop` no longer deletes the file, SessionStart asks the host's `source` (`startup`/`clear`/`compact`/`resume`) instead of guessing from the file, and the handoff's prompt lookup falls back to the unscoped set when the new session's id matches none. Revert path: `CLAUDE_MEM_LEGACY_STOP_UNLINK=1`
125
125
  - **Git-SHA continuation anchor** (v2.31.0) -- Handoff rows include `git_sha_at_handoff`; any handoff matching the current `HEAD` counts as continuation regardless of TTL. Code state is a stronger continuation signal than wall-clock time
126
126
  - **Startup dashboard** (v2.31.0) -- SessionStart hook aggregates `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + most-recent exit handoff + recent event count into a single structured block injected via `hookSpecificOutput.additionalContext`
127
- - **Activity namespace** (v2.31.0) -- Dedicated `events` table + FTS5 for non-memdir types (`bugfix`, `lesson`, `bug`, `discovery`, `refactor`, `feature`, `observation`, `decision`) that don't compete with `WHAT_NOT_TO_SAVE` semantics on the observations table. CLI: `claude-mem-lite activity save|search|recent|show`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic. (v3.39: the `/lesson` and `/bug` slash commands were redirected from this events table to searchable **observations** — `mem_search` never read the events table, so explicit saves were unfindable; the events table remains the auto-capture activity log.)
127
+ - **Activity namespace** (v2.31.0) -- Dedicated `events` table + FTS5 for non-memdir types (`bugfix`, `lesson`, `bug`, `discovery`, `refactor`, `feature`, `observation`, `decision`) that don't compete with `WHAT_NOT_TO_SAVE` semantics on the observations table. CLI: `claude-mem-lite activity save|search|recent|show`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic. (v3.39: the `/lesson` and `/claude-mem-lite:bug` slash commands were redirected from this events table to searchable **observations** — `mem_search` never read the events table, so explicit saves were unfindable; the events table remains the auto-capture activity log.)
128
128
  - **In-place observation updates** -- `mem_update` tool modifies existing observations atomically (field update + FTS text rebuild in one transaction), preserving original IDs and references
129
129
  - **Bulk export** -- `mem_export` tool exports observations as JSON or JSONL, with project/type/date filtering and 1000-row pagination cap with batch guidance
130
130
  - **FTS integrity management** -- `mem_fts_check` tool verifies FTS5 index health or rebuilds indexes on demand, useful after database recovery or when search results seem wrong
@@ -165,7 +165,7 @@ is still outside it gets a message naming both sides of the mismatch instead of
165
165
 
166
166
  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.
167
167
 
168
- > **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 (`node "${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.
168
+ > **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/claude-mem-lite: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 (`node "${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.
169
169
 
170
170
  > **Auto-adopt no longer adds its block to your project's `CLAUDE.md` (next release after 6.19.4).** On every SessionStart the plugin delivers its steering text — the trigger table that boosts Claude's proactive use of `mem_recall` / `mem_save`. In a git repository that does not already carry the block it keeps a managed block in `CLAUDE.local.md` at the repository root (Claude Code loads that file like `CLAUDE.md`, also in subagents that load project instructions; the built-in Explore and Plan agents load none) and adds the file to the repository's `.git/info/exclude` unless your ignore rules already cover it, so git does not list or commit it; you get a **one-time notice** the first time it is written. Claude Code reads instruction files before the plugin's startup hook runs, so the session that creates the file gets the text added to its context instead, once (its subagents do not see it). Where Claude Code reads an `AGENTS.md` as the project's instructions, it stops reading it once a `CLAUDE.md` or `CLAUDE.local.md` exists in the session's directory or above, so there nothing is written and the text is **injected** (subagents do not see it), with a one-time notice that names that `AGENTS.md`. With `CLAUDE_MEM_RULES_STEERING=1` the block goes to `.claude/rules/claude-mem-lite.md` at the repository root instead — excluded from git the same way, loaded by Claude Code as project instructions, also in subagents that load project instructions, and it leaves `AGENTS.md` loading — with its own one-time notice; it is not the default because in a pre-registered A/B it did not beat injection by the bar set in advance (`docs/audits/20261006-d212-ab.md`). Outside git, in a repository rooted at `$HOME`, where `CLAUDE.local.md` is tracked or a symbolic link, or where the repository root is an npm package that `npm publish` would ship it with (no `"private": true`, no `files` list leaving it out, and without one no `.npmignore` naming it), nothing is written and the text is **injected** into each session's context, with a one-time notice suggesting `/adopt` (not at `$HOME`). With the variable set, the same holds where the rules file cannot be written (a symbolic link on its path, a tracked or foreign file of that name, an npm package that would ship it). The notice beside an `AGENTS.md` names it, why there is no file, and the ways to a file that keep it loading, each only where it works: Project instructions = `claude-md-and-agents-md` in `/config`, after which `CLAUDE.local.md` is written (not where `CLAUDE.local.md` is refused too: a package root, or a file you removed), and `/adopt`, whose `CLAUDE.md` imports the `AGENTS.md` beside it (not offered when an `AGENTS.md` above that directory or in a subdirectory would stop loading). The detail file the block points at lives in the plugin's data directory, named with a `~/` path. `.git/info/exclude` is read by git only: docker build contexts, archives and other packagers can pick `CLAUDE.local.md` or the rules file up (a publishable npm package root does not keep `CLAUDE.local.md`: a block written before is taken out at the next session start; in a project whose `AGENTS.md` it would switch off, the block comes out and is injected, or, with `CLAUDE_MEM_RULES_STEERING=1`, moves to the rules file where that can be written). Earlier versions added the block to your project's own `<cwd>/CLAUDE.md` (a file that is normally committed) plus a `<cwd>/.claude/plugin_claude_mem_lite.md` detail file, unasked, in every project you opened; those projects keep the block, and the first session of this version refreshes it because the text changed — to move one to the local file, run `claude-mem-lite unadopt` there and commit the removal. Why not inject everywhere: in our sandbox evaluation (one project, Claude Opus 5.5) the agent made 1.5 proactive memory saves per 8-session run with injected steering against 5.25 with the block in `CLAUDE.local.md` or `CLAUDE.md`, and subagents never saw injected text. It applies regardless of install path (npm, npx, `/plugin`, manual); silence these notices with `MEM_NO_ADOPT_HINT=1`.
171
171
  >
@@ -684,9 +684,9 @@ have not allowed, and routing the same action through Bash would sidestep it.
684
684
  /mem timeline <query> # Browse timeline around a match
685
685
  /mem browse # Tier-grouped memory dashboard
686
686
  /mem <query> # Shorthand for search
687
- /lesson <text> # Save a non-obvious lesson to the events table (v2.31.0)
688
- /bug <text> # Log a known bug + repro steps to the events table (v2.31.0)
689
- /verify # Check memories against the current code; correct stale ones after you approve
687
+ /lesson <text> # Save a non-obvious lesson as a searchable observation
688
+ /claude-mem-lite:bug <text> # Log a known bug + repro steps as a searchable observation
689
+ /claude-mem-lite:verify # Check memories against the current code; correct stale ones after you approve
690
690
  ```
691
691
 
692
692
  ### Efficient Search Workflow
@@ -701,12 +701,10 @@ have not allowed, and routing the same action through Bash would sidestep it.
701
701
 
702
702
  Opt-in mechanism that installs a slug-scoped managed block into the project's
703
703
  own `CLAUDE.md` (plus an on-demand detail doc under `.claude/`) so Claude Code
704
- loads the plugin's MCP-tool triggers as **project instructions** — a higher
705
- instruction-following authority than MCP server instructions (which are framed
706
- as tool metadata). Claude Code loads `CLAUDE.md` and the memdir `MEMORY.md` at
707
- equal weight, so steering lives in `CLAUDE.md` (the canonical home for project
708
- instructions) rather than polluting `MEMORY.md`, which is reserved for the user's
709
- own memories. The pre-v3.13 scheme wrote into `MEMORY.md`; it is migrated away
704
+ loads the plugin's MCP-tool triggers as **project instructions**, delivered in a
705
+ message after the system prompt. Steering lives in `CLAUDE.md` (the canonical home
706
+ for project instructions) rather than in the memdir `MEMORY.md`, which is reserved
707
+ for the user's own memories. The pre-v3.13 scheme wrote into `MEMORY.md`; it is migrated away
710
708
  automatically on the next SessionStart.
711
709
 
712
710
  ```bash
package/README.zh-CN.md CHANGED
@@ -95,7 +95,7 @@
95
95
  - **插件缓存 hook 自愈** -- Claude Code runtime 从 `~/.claude/plugins/cache/<mp>/<plugin>/<ver>/hooks/hooks.json` 读取插件 hook,而非 marketplace 源。当 `install.mjs` 写入 `settings.json` 的 hooks 与残留 cache `hooks.json` 同时存在(例如曾装过 marketplace 版本,或插件被 Claude Code 自动升级重建 cache),runtime 会注册两套 hook → 每次 SessionStart / UserPromptSubmit 都触发两份。`install.mjs` 和 `hook-update.mjs` 现在会清理每个 cache 版本目录下的 `hooks.json`;`hook.mjs session-start` 每次启动自愈(通过 `hasInstallManagedHooks` 门控,不影响纯插件模式用户);`install.mjs status` 会报告 cache 污染状况(自 v2.31.1 / v2.31.2 起)。
96
96
  - **Git-SHA 延续锚点**(v2.31.0)-- handoff 记录包含 `git_sha_at_handoff` 字段,任何匹配当前 `HEAD` 的 handoff 都视为延续会话,不受 TTL 限制。代码状态比时钟时间更能反映上下文延续。
97
97
  - **启动面板**(v2.31.0)-- SessionStart hook 将 `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + 最近 /exit 交接 + 最近事件数聚合为一个结构化块,通过 `hookSpecificOutput.additionalContext` 注入。
98
- - **活动命名空间**(v2.31.0)-- 为非 memdir 类型(`bugfix` / `lesson` / `bug` / `discovery` / `refactor` / `feature` / `observation` / `decision`)启用独立的 `events` 表 + FTS5,与 observations 表的 `WHAT_NOT_TO_SAVE` 语义解耦。CLI:`claude-mem-lite activity save|search|recent|show`。`hook-llm` 通过 `persistHaikuSummary` 路由非 memdir 摘要,observations → events 升级路径是事务原子的。(v3.39:`/lesson`、`/bug` 斜杠命令已从此 events 表重定向到可搜索的 **observations**——`mem_search` 从不读 events 表,显式保存因此搜不到;events 表仍作自动捕获活动日志。)
98
+ - **活动命名空间**(v2.31.0)-- 为非 memdir 类型(`bugfix` / `lesson` / `bug` / `discovery` / `refactor` / `feature` / `observation` / `decision`)启用独立的 `events` 表 + FTS5,与 observations 表的 `WHAT_NOT_TO_SAVE` 语义解耦。CLI:`claude-mem-lite activity save|search|recent|show`。`hook-llm` 通过 `persistHaikuSummary` 路由非 memdir 摘要,observations → events 升级路径是事务原子的。(v3.39:`/lesson`、`/claude-mem-lite:bug` 斜杠命令已从此 events 表重定向到可搜索的 **observations**——`mem_search` 从不读 events 表,显式保存因此搜不到;events 表仍作自动捕获活动日志。)
99
99
 
100
100
  ## 平台支持
101
101
 
@@ -129,7 +129,7 @@ v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`
129
129
 
130
130
  插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
131
131
 
132
- > **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI(`node "${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`。插件要完整工作**并不需要**它。
132
+ > **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/claude-mem-lite:bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI(`node "${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`。插件要完整工作**并不需要**它。
133
133
 
134
134
  ### 方式二:npx(一行命令)
135
135
 
@@ -533,9 +533,9 @@ README 和 `docs/ARCHITECTURE.md` 都钉在它上面。)
533
533
  /mem timeline <query> # 围绕匹配结果浏览时间线
534
534
  /mem browse # 分层记忆仪表盘
535
535
  /mem <query> # search 的简写
536
- /lesson <text> # 保存非显而易见的经验到 events 表(v2.31.0)
537
- /bug <text> # 记录已知 bug + 复现步骤到 events 表(v2.31.0)
538
- /verify # 对照当前代码核查记忆;你确认后才改正过期的记忆
536
+ /lesson <text> # 把非显而易见的经验存为可搜索的 observation
537
+ /claude-mem-lite:bug <text> # 把已知 bug + 复现步骤记为可搜索的 observation
538
+ /claude-mem-lite:verify # 对照当前代码核查记忆;你确认后才改正过期的记忆
539
539
  ```
540
540
 
541
541
  ### 高效搜索工作流
@@ -548,10 +548,11 @@ README 和 `docs/ARCHITECTURE.md` 都钉在它上面。)
548
548
 
549
549
  ### Invited Memory(邀请式记忆,v2.32+)
550
550
 
551
- Opt-in 机制——向项目 memdir(`~/.claude/projects/<encoded>/memory/MEMORY.md`)
552
- 注入单行 sentinel 包围的插件契约,Claude Code 会把它作为 **user-memory** 加载
553
- 到系统提示——比 MCP server instructions(被框定为 tool metadata)具有更高的
554
- instruction-following 权威。
551
+ Opt-in 机制——在项目自己的 `CLAUDE.md` 里写入一个按 slug 限定的托管块(外加 `.claude/` 下
552
+ 按需读取的详情文档),让 Claude Code 把插件的 MCP 工具触发规则作为**项目指令**加载(在系统
553
+ 提示之后的一条消息里送达)。引导放在 `CLAUDE.md`(项目指令的正规位置),不去占用 memdir 里
554
+ 留给用户自己记忆的 `MEMORY.md`。v3.13 之前的方案写入
555
+ `MEMORY.md`;下一次 SessionStart 会自动迁走。
555
556
 
556
557
  ```bash
557
558
  claude-mem-lite adopt # 当前项目注入
@@ -566,16 +567,19 @@ claude-mem-lite unadopt # 移除 CLAUDE.md 托管块和详情文档
566
567
  Slash 命令 `/adopt` 和 `/unadopt` 是上述 CLI 的包装。
567
568
 
568
569
  **Adopt 后会发生什么:**
569
- - `MEMORY.md` 新增一段 `<!-- claude-mem-lite:begin v1 -->…<!-- claude-mem-lite:end -->`
570
- 包裹的 `## 插件契约`,含一行 ≤150 字符、指向 `mem_recall` / `mem_save` 关键参数
571
- 的动作锚条目。
572
- - 生成 `plugin_claude_mem_lite.md` 详情文件(按需读取,不自动加载)。
570
+ - `<cwd>/CLAUDE.md` 新增一段 `<!-- claude-mem-lite:begin v1 -->…<!-- claude-mem-lite:end -->`
571
+ 包裹、带独立 `## claude-mem-lite — persistent memory` 标题的托管块,内含指向
572
+ `mem_recall` / `mem_save` / `mem_defer` 及其关键参数的触发表。只管理这一段,`CLAUDE.md`
573
+ 其余内容原样保留,可与其他插件(如 `code-graph-mcp`)的托管块共存于同一文件。
574
+ - 生成 `<cwd>/.claude/plugin_claude_mem_lite.md` 详情文件(不自动加载;`CLAUDE.md` 托管块
575
+ 指向它时按需读取)。出货内容变化(版本升级或模板变更)时托管块自动刷新,除非设
576
+ `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1`。
573
577
  - 保守 hook 层自动瘦身:MCP server instructions 去掉 `WHEN TO USE` 段,
574
578
  SessionStart 注入去掉 `File Lessons` / `Key Context`。`#ID` 引用与 `Recent`
575
579
  表保留,`mem_get` 仍可随时展开。
576
580
 
577
581
  **什么时候生效?**
578
- - MEMORY.md sentinel 和 hook 层瘦身(`File Lessons` / `Key Context` / lesson
582
+ - `CLAUDE.md` 托管块和 hook 层瘦身(`File Lessons` / `Key Context` / lesson
579
583
  后缀)**下一次 SessionStart** 生效(adopt 的项目下任一新会话)。
580
584
  - MCP server instructions 是**服务启动时构建一次**,MCP 协议无 "push" 机制,
581
585
  所以 `WHEN TO USE` / `Decision rules` 两段的瘦身**只在 Claude Code 重启后**
package/adopt-cli.mjs CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  claudeMdPath,
51
51
  detailDocPath,
52
52
  addAgentsImports,
53
- holdsOnlyOwnLines,
53
+ holdsOnlyPluginLines,
54
54
  straySentinelLines,
55
55
  } from './claudemd.mjs';
56
56
  import { PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc } from './adopt-content.mjs';
@@ -77,7 +77,9 @@ import {
77
77
  isSharedAncestor,
78
78
  samePath,
79
79
  localFileLinked,
80
+ userInstructionFiles,
80
81
  } from './lib/local-steering.mjs';
82
+ import { CLI_INVOKE } from './cli-path.mjs';
81
83
 
82
84
  /**
83
85
  * Remove the auto-written local block (CLAUDE.local.md, or the D#212 rules file) for the project at
@@ -226,7 +228,7 @@ function adoptOne(cwd, { force, dryRun }) {
226
228
  const sharedNote = (verb) =>
227
229
  switchesOffBelow
228
230
  ? [
229
- ` ⚠ ${claudeMdPath(cwd)} ${verb} Claude Code reading the AGENTS.md of every project below ${cwd} that has one; to steer one project, run adopt inside it instead${verb === 'stops' ? ' (`claude-mem-lite unadopt` here takes this back)' : ''}, or set Project instructions to claude-md-and-agents-md in /config`,
231
+ ` ⚠ ${claudeMdPath(cwd)} ${verb} Claude Code reading the AGENTS.md of every project below ${cwd} that has one; to steer one project, run adopt inside it instead${verb === 'stops' ? ` (\`${CLI_INVOKE} unadopt\` here takes this back)` : ''}, or set Project instructions to claude-md-and-agents-md in /config`,
230
232
  ]
231
233
  : [];
232
234
 
@@ -262,7 +264,12 @@ function adoptOne(cwd, { force, dryRun }) {
262
264
  : local.action === 'failed'
263
265
  ? ` (could not remove the block from ${local.path}, so sessions here load it twice)`
264
266
  : ` (+removed the block from ${local.path})`;
265
- log(`[adopt] ${cwd} → ${r.action}${migNote}${localNote}${importNote}`);
267
+ // The block's disposition alone read "unchanged" over a rewritten detail doc (D#470).
268
+ const docNote =
269
+ r.action === 'unchanged' && r.docAction !== 'unchanged'
270
+ ? ` (+${r.docAction === 'created' ? 'wrote' : 'refreshed'} detail doc)`
271
+ : '';
272
+ log(`[adopt] ${cwd} → ${r.action}${docNote}${migNote}${localNote}${importNote}`);
266
273
  for (const line of elsewhere('stops')) log(line);
267
274
  for (const line of sharedNote('stops')) log(line);
268
275
  // The copy that could not come out loads beside CLAUDE.md: said, and not a success (R3-4).
@@ -373,13 +380,17 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
373
380
  // cost most of the proactive memory writes (1.5 vs 5.25 per trajectory) and never reached
374
381
  // subagents (0/12). Inside a git work tree the block now goes to CLAUDE.local.md, which the
375
382
  // host loads like CLAUDE.md and info/exclude keeps out of commits (5.25 writes, 12/12).
376
- const hasBlock = blockIn(cwd);
383
+ // managed-only loads no project CLAUDE.md either (code.claude.com/docs/en/memory): a block in
384
+ // one steers nothing, so the session goes on to the refused local file and injects, and the
385
+ // file stays as it is (v6.30.0 pre-tag defects review P2-2).
386
+ const managedOnly = userInstructionFiles() === 'managed-only';
387
+ const hasBlock = !managedOnly && blockIn(cwd);
377
388
  if (!hasBlock) {
378
389
  if (markerDir && markerKey) writeMarker(markerDir, markerKey);
379
390
  // A damaged sentinel line (`… --> (team note)`) leaves the block's text in CLAUDE.md, which
380
391
  // the host loads, while the block no longer parses: a copy written to CLAUDE.local.md or
381
392
  // injected here was a second one. Neither; `adopt` names the line and the fix (D#369 B14).
382
- const stray = straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
393
+ const stray = managedOnly ? [] : straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
383
394
  if (stray.length > 0) return { ok: true, action: 'damaged', reason: 'stray-sentinel', lines: stray };
384
395
  const root = localSteeringRoot(cwd);
385
396
  if (root) {
@@ -387,7 +398,13 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
387
398
  // loads the root's CLAUDE.md as an ancestor, so a block there already steers this
388
399
  // session; and an opt-out recorded for the root covers the file that lives there.
389
400
  if (!samePath(root, cwd)) {
390
- if (blockIn(root)) return { ok: true, action: 'already-adopted', reason: 'root-claude-md' };
401
+ const above = managedOnly ? null : blockAbove(cwd, root);
402
+ if (above)
403
+ return {
404
+ ok: true,
405
+ action: 'already-adopted',
406
+ reason: samePath(above, root) ? 'root-claude-md' : 'ancestor-claude-md',
407
+ };
391
408
  // Off for the project means off here too: no file, no injected copy, no /adopt offer
392
409
  // (delta review P2-2; lib/quiet-scope.mjs mirrors it).
393
410
  if (isAutoAdoptDisabledFor(root)) return { ok: true, action: 'disabled', reason: 'root-disabled' };
@@ -421,7 +438,11 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
421
438
  agentsMd: r.agentsMd,
422
439
  };
423
440
  }
424
- return { ok: true, action: 'inject' };
441
+ // managed-only loads no project CLAUDE.md, so the hook does not offer /adopt (v6.30.0 pre-tag
442
+ // claims review P2-5; inside a git work tree the reason is local-managed-only).
443
+ return managedOnly
444
+ ? { ok: true, action: 'inject', reason: 'managed-only' }
445
+ : { ok: true, action: 'inject' };
425
446
  }
426
447
  dropLocalSteering(cwd, { atRootOnly: true, keepTracked: true });
427
448
  let action = 'already-adopted';
@@ -462,8 +483,8 @@ function syncAgentsImports(cwd) {
462
483
  // project below it, each asking to approve an import from outside its directory.
463
484
  if (isSharedAncestor(cwd)) return null;
464
485
  // The cheap check first: most CLAUDE.md files hold the user's own lines, and the full answer
465
- // asks git.
466
- if (!holdsOnlyOwnLines(claudeMdPath(cwd), PLUGIN_SLUG)) return null;
486
+ // asks git. Another plugin's managed block counts as plugin text, not the user's (D#220).
487
+ if (!holdsOnlyPluginLines(claudeMdPath(cwd), PLUGIN_SLUG)) return null;
467
488
  const agents = agentsMdForNewClaudeMd(cwd, PLUGIN_SLUG);
468
489
  if (!agents) return null;
469
490
  const imported =
@@ -501,6 +522,16 @@ function blockIn(dir) {
501
522
  }
502
523
  }
503
524
 
525
+ // The nearest directory above `cwd`, up to and including `root`, whose CLAUDE.md carries the block:
526
+ // the host loads it as an ancestor. `adopt` run in packages/ and a session in packages/web/src got a
527
+ // root CLAUDE.local.md on top of packages/CLAUDE.md, the block twice (D#177(2)).
528
+ function blockAbove(cwd, root) {
529
+ for (let d = dirname(cwd); ; d = dirname(d)) {
530
+ if (blockIn(d)) return d;
531
+ if (samePath(d, root) || dirname(d) === d) return null;
532
+ }
533
+ }
534
+
504
535
  function writeMarker(markerDir, markerKey) {
505
536
  if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
506
537
  const path = join(markerDir, `.auto-adopt-${markerKey}`);
@@ -620,9 +651,11 @@ function cmdEnable(args) {
620
651
  function localSteeringStatus(cwd) {
621
652
  const root = localSteeringRoot(cwd);
622
653
  const runs = autoAdoptRuns(cwd, root);
623
- // CLAUDE.md first: where it carries the block, no local file is written and nothing is injected,
624
- // in a git work tree or not (pre-tag delta review of the D#212 repairs, D11).
625
- if (blockIn(cwd) || (root && blockIn(root))) {
654
+ // CLAUDE.md first: where it carries the block, no local file is written, in a git work tree or not
655
+ // (pre-tag delta review of the D#212 repairs, D11).
656
+ if (blockIn(cwd) || (root && !samePath(root, cwd) && blockAbove(cwd, root))) {
657
+ if (userInstructionFiles() === 'managed-only')
658
+ return '⚠ CLAUDE.md carries the block, but Project instructions is set to managed-only, which does not load it';
626
659
  const cur = root ? readLocalSteering(root, PLUGIN_SLUG) : null;
627
660
  if (cur === null || cur.body === null) return '— none: CLAUDE.md carries the block';
628
661
  const kept = localFileLinked(root, cur.path)
@@ -642,7 +675,7 @@ function localSteeringStatus(cwd) {
642
675
  }
643
676
  // Before "not a git work tree": with auto-adopt off nothing is injected either (R3-13).
644
677
  if (isAutoAdoptDisabledFor(cwd) || (root && isAutoAdoptDisabledFor(root)))
645
- return '— none: auto-adopt is off for this project (`claude-mem-lite adopt --enable` turns it back on)';
678
+ return `— none: auto-adopt is off for this project (\`${CLI_INVOKE} adopt --enable\` turns it back on)`;
646
679
  if (process.env.MEM_NO_AUTO_ADOPT === '1')
647
680
  return '— none: MEM_NO_AUTO_ADOPT=1, so no local file is written';
648
681
  if (!root)
@@ -656,10 +689,10 @@ function localSteeringStatus(cwd) {
656
689
  const plan = planLocalSteering(root, PLUGIN_SLUG, cwd);
657
690
  const beside = plan.agentsMd ?? 'the AGENTS.md';
658
691
  if (plan.file === RULES_MD && plan.refusal === 'off')
659
- return `${removed}; beside ${beside} a file comes back only with CLAUDE_MEM_RULES_STEERING=1 and then \`claude-mem-lite adopt --enable\``;
692
+ return `${removed}; beside ${beside} a file comes back only with CLAUDE_MEM_RULES_STEERING=1 and then \`${CLI_INVOKE} adopt --enable\``;
660
693
  if (plan.file === RULES_MD && plan.refusal)
661
694
  return `${removed}; beside ${beside}, ${rulesWhy(plan.refusal)}`;
662
- return `${removed}; after \`claude-mem-lite adopt --enable\` the next session may write it again`;
695
+ return `${removed}; after \`${CLI_INVOKE} adopt --enable\` the next session may write it again`;
663
696
  }
664
697
  const plan = planLocalSteering(root, PLUGIN_SLUG, cwd);
665
698
  if (!plan.refusal)
@@ -671,7 +704,7 @@ function localSteeringStatus(cwd) {
671
704
  ways.settingGivesFile
672
705
  ? 'set Project instructions to claude-md-and-agents-md in /config and CLAUDE.local.md is written instead'
673
706
  : null,
674
- ways.adoptImports ? 'run `claude-mem-lite adopt`, whose CLAUDE.md imports AGENTS.md' : null,
707
+ ways.adoptImports ? `run \`${CLI_INVOKE} adopt\`, whose CLAUDE.md imports AGENTS.md` : null,
675
708
  ].filter(Boolean);
676
709
  return `✗ not written: Claude Code stops reading ${plan.agentsMd ?? 'the AGENTS.md'} once a CLAUDE.local.md exists, and ${rulesWhy(plan.refusal)} (steering is injected at session start${alt.length ? `; ${alt.join(', or ')}` : ''})`;
677
710
  }
@@ -685,6 +718,9 @@ function currentLocalLine(root, cwd, cur, runs) {
685
718
  return `✓ ${cur.path} (behind a symbolic link; the plugin leaves it as it is)`;
686
719
  if (trackedByGit(root, rel)) return `✓ ${cur.path} (tracked by git; the plugin leaves it as it is)`;
687
720
  const plan = runs ? planLocalSteering(root, PLUGIN_SLUG, cwd) : null;
721
+ // Left in place under managed-only (writeLocalSteering, D#219), not removed.
722
+ if (plan?.refusal === 'managed-only')
723
+ return `⚠ ${cur.path} (auto-written, but CLAUDE.local.md ${LOCAL_REFUSAL_TEXT['managed-only']}; the steering is injected, and the file stays for when the setting changes)`;
688
724
  if (plan?.refusal) {
689
725
  const why =
690
726
  plan.file === LOCAL_MD
@@ -764,7 +800,7 @@ function statusAll() {
764
800
  `[adopt --status] scanned ${dirs.length} memdir(s): ${legacy} with legacy sentinel (await migration), ${disabled} auto-adopt-disabled.`,
765
801
  );
766
802
  if (legacy > 0)
767
- log('[adopt --status] run `claude-mem-lite adopt --all` to sweep legacy memory-dir sentinels now.');
803
+ log(`[adopt --status] run \`${CLI_INVOKE} adopt --all\` to sweep legacy memory-dir sentinels now.`);
768
804
 
769
805
  const known = listKnownProjectDirs();
770
806
  let adoptedCount = 0;
@@ -773,7 +809,7 @@ function statusAll() {
773
809
  `[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block or partial residue (detail doc/state).`,
774
810
  );
775
811
  if (adoptedCount > 0)
776
- log('[adopt --status] run `claude-mem-lite unadopt --all` to remove every CLAUDE.md block.');
812
+ log(`[adopt --status] run \`${CLI_INVOKE} unadopt --all\` to remove every CLAUDE.md block.`);
777
813
 
778
814
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
779
815
  const noAutoAdopt = process.env.MEM_NO_AUTO_ADOPT === '1' ? '1 (opt-out)' : 'unset';
@@ -783,7 +819,7 @@ function statusAll() {
783
819
  ` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (any install path is consent; gate is the per-project opt-out below)`,
784
820
  );
785
821
  log(` MEM_NO_AUTO_ADOPT = ${noAutoAdopt} (global escape hatch)`);
786
- log('Per-project opt-out: `claude-mem-lite adopt --disable` (run --enable to re-arm).');
822
+ log(`Per-project opt-out: \`${CLI_INVOKE} adopt --disable\` (run --enable to re-arm).`);
787
823
  }
788
824
 
789
825
  /**
@@ -852,7 +888,7 @@ function unadoptAll(args) {
852
888
  if (failures > 0) process.exitCode = 1;
853
889
  if (projectDirs.length === 0) {
854
890
  log(
855
- '[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run `claude-mem-lite unadopt` from inside it.',
891
+ `[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run \`${CLI_INVOKE} unadopt\` from inside it.`,
856
892
  );
857
893
  }
858
894
  }
package/adopt-content.mjs CHANGED
@@ -73,7 +73,6 @@ export function getDetailDoc() {
73
73
 
74
74
  > 由 \`${CLI} adopt\` 生成、随版本自动刷新;卸载用 \`${CLI} unadopt\`。
75
75
  > 精炼触发表由 SessionStart 注入会话上下文(显式 \`${CLI} adopt\` 过的项目则写在 \`CLAUDE.md\` 的 \`claude-mem-lite\` 托管块里);本文件是其展开。
76
- > 设计背景见 docs/CLAUDE-MD-STEERING-PLAN.md。
77
76
 
78
77
  > **本文下方所有命令写作 \`${CLI} <cmd>\`。** 该名字只在全局装过
79
78
  > (\`npm i -g claude-mem-lite\`)时才在 PATH 上;否则用等价的
@@ -159,7 +158,7 @@ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该
159
158
 
160
159
  | 场景 | CLI |
161
160
  |------|-----|
162
- | 清理过期记忆 | \`${CLI} maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale --confirm\`(删行必须 \`--confirm\`) |
161
+ | 清理过期记忆 | \`${CLI} maintain scan\` 预览;清理本身交给用户启动的 \`/claude-mem-lite:update\` |
163
162
  | 深度优化(Haiku) | \`${CLI} optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\`) |
164
163
  | 压缩旧条目 | \`${CLI} compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\`) |
165
164
  | FTS5 索引检查 / 重建 | \`${CLI} fts-check <check\\|rebuild>\` |
package/claudemd.mjs CHANGED
@@ -2,9 +2,8 @@
2
2
  // project-tree managed block at <cwd>/CLAUDE.md plus an on-demand detail doc
3
3
  // at <cwd>/.claude/plugin_<slug>.md.
4
4
  //
5
- // Why here and not memdir.mjs: Claude Code loads the project CLAUDE.md and the
6
- // memory-dir MEMORY.md at EQUAL weight, so steering belongs in CLAUDE.md (the
7
- // canonical home for project instructions) — seeding MEMORY.md just pollutes an
5
+ // Why here and not memdir.mjs: steering belongs in CLAUDE.md (the canonical home
6
+ // for project instructions) — seeding the memory-dir MEMORY.md just pollutes an
8
7
  // index meant for the user's own memories. This module mirrors the design
9
8
  // shipped by the sibling code-graph-mcp plugin (claude-plugin/scripts/adopt.js)
10
9
  // and the oh-my-claudecode versioned `<!-- :begin vN -->` managed-block pattern.
@@ -182,6 +181,31 @@ export function holdsOnlyOwnLines(p, slug) {
182
181
  }
183
182
  }
184
183
 
184
+ // Another plugin's managed block: the same sentinel shape under any slug (code-graph-mcp writes
185
+ // `<!-- code-graph-mcp:begin v2 -->` … `<!-- code-graph-mcp:end -->`). The backreference pairs a
186
+ // begin with its own slug's end, so an unpaired line of one slug never closes another's block.
187
+ const ANY_BLOCK_RE_G =
188
+ /<!-- ([a-z0-9][a-z0-9._-]*):begin v\d+ -->\r?\n(?:(?!<!-- \1:(?:begin|end))[\s\S])*?\r?\n<!-- \1:end -->/g;
189
+
190
+ /**
191
+ * Whether the instructions file at `p` holds no line a person wrote: this plugin's blocks and
192
+ * imports, and other plugins' managed blocks. For "did plugin text alone switch AGENTS.md off"
193
+ * (D#220), never for deleting the file — another plugin's block is not ours to remove, and
194
+ * holdsOnlyOwnLines answers that question. False for a missing file.
195
+ * @param {string} p absolute path
196
+ * @param {string} slug
197
+ * @returns {boolean}
198
+ */
199
+ export function holdsOnlyPluginLines(p, slug) {
200
+ try {
201
+ if (!existsSync(p)) return false;
202
+ const rest = readFileSync(p, 'utf8').replace(blockRegexG(slug), '').replace(ANY_BLOCK_RE_G, '');
203
+ return rest.trim() === '' || isOwnImportsOnly(rest, slug);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
185
209
  /**
186
210
  * Put adopt's AGENTS.md imports at the top of <cwd>/CLAUDE.md (created if absent), unless the
187
211
  * marker is there already. `imports` are paths relative to `cwd` (`AGENTS.md`,
@@ -366,7 +390,8 @@ export function needsRefresh(cwd, { slug, version, block, doc }) {
366
390
  * CLAUDE.md is created if absent. Only our slug-scoped region is rewritten;
367
391
  * everything else in the file is preserved verbatim.
368
392
  *
369
- * @returns {{action: 'created'|'updated'|'unchanged'}} (block disposition)
393
+ * @returns {{action: 'created'|'updated'|'unchanged', docAction: 'created'|'updated'|'unchanged'}}
394
+ * (block disposition, and the detail doc's)
370
395
  */
371
396
  export function writeManaged(cwd, { slug, version, block, doc }) {
372
397
  const { action } = writeBlockAt(claudeMdPath(cwd), { slug, version, block });
@@ -379,6 +404,7 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
379
404
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
380
405
  const existingDoc = existsSync(dp) ? readFileSync(dp, 'utf8') : null;
381
406
  if (existingDoc !== docContent) atomicWrite(dp, docContent);
407
+ const docAction = existingDoc === null ? 'created' : existingDoc === docContent ? 'unchanged' : 'updated';
382
408
 
383
409
  // Only when what the sidecar records changed: it is committed alongside the block in a team
384
410
  // repo, and a fresh writtenAt on every re-run of `adopt` left a diff each time (E2E
@@ -394,7 +420,7 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
394
420
  if (!(prev?.version === version && prev?.blockHash === blockHash && prev?.docHash === docHash)) {
395
421
  writeState(cwd, slug, { version, blockHash, docHash, writtenAt: new Date().toISOString() });
396
422
  }
397
- return { action };
423
+ return { action, docAction };
398
424
  }
399
425
 
400
426
  /**
package/cli/activity.mjs CHANGED
@@ -15,6 +15,7 @@ import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
15
15
  import { parseIntFlag, isNumericToken } from '../lib/cli-flags.mjs';
16
16
  import { snapshotDb } from '../lib/db-backup.mjs';
17
17
  import { isBlankText } from '../lib/blank-text.mjs';
18
+ import { CLI_INVOKE } from '../cli-path.mjs';
18
19
 
19
20
  function formatActivityResults(rows) {
20
21
  if (!rows || rows.length === 0) return '(no events)';
@@ -31,7 +32,7 @@ function eventIdOf(token) {
31
32
  export async function cmdActivity(db, args) {
32
33
  const sub = args[0];
33
34
  if (!sub) {
34
- fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show|delete|promote> ...');
35
+ fail(`[mem] Usage: ${CLI_INVOKE} activity <save|search|recent|show|delete|promote> ...`);
35
36
  return;
36
37
  }
37
38
 
@@ -171,7 +172,7 @@ export async function cmdActivity(db, args) {
171
172
  // This command lets users prune them by ID without dropping to raw SQL.
172
173
  const idStr = positional.join(',').trim();
173
174
  if (!idStr) {
174
- fail('[mem] Usage: claude-mem-lite activity delete <id1,id2,...> [--confirm]');
175
+ fail(`[mem] Usage: ${CLI_INVOKE} activity delete <id1,id2,...> [--confirm]`);
175
176
  return;
176
177
  }
177
178
  const tokens = idStr
package/cli/common.mjs CHANGED
@@ -9,7 +9,8 @@
9
9
  // consistent.
10
10
 
11
11
  import { editDistance } from '../lib/edit-distance.mjs';
12
- import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from '../format-utils.mjs';
12
+ import { neutralizeContextDelimiters, neutralizeSkillDelimiters, truncate } from '../format-utils.mjs';
13
+ import { CLI_INVOKE } from '../cli-path.mjs';
13
14
 
14
15
  // ─── Argument Parsing ────────────────────────────────────────────────────────
15
16
 
@@ -250,7 +251,7 @@ export function inertFilterFlagNotice(cmd, inert) {
250
251
  const verb = inert.length > 1 ? 'were' : 'was';
251
252
  return (
252
253
  `[mem] ${names} ${verb} ignored — \`${cmd}\` does not filter on ${inert.length > 1 ? 'them' : 'it'}, ` +
253
- `so the results above are UNFILTERED. Run "claude-mem-lite help" for the flags this command reads.`
254
+ `so the results above are UNFILTERED. Run "${CLI_INVOKE} help" for the flags this command reads.`
254
255
  );
255
256
  }
256
257
 
@@ -596,11 +597,16 @@ export function obsFieldLabel(field) {
596
597
  * 2026-08-14 A4). Say what the rows are and what deletes them — and say nothing about
597
598
  * compression, which is a different sentinel with a different lifecycle.
598
599
  *
600
+ * purge_stale deletes only the queued rows older than its retain window (D#450), so the line
601
+ * also gives the number a purge at the default window deletes now.
602
+ *
599
603
  * @param {number} n stats.pendingPurge
604
+ * @param {number} due stats.pendingPurgeDue
605
+ * @param {number} retainDays the default retain window `due` was counted at
600
606
  * @returns {string} the full indented line, identical on both surfaces.
601
607
  */
602
- export function formatPendingPurgeLine(n) {
603
- return ` Pending purge (idle-marked): ${n} (rows maintenance queued: hidden and still idle 7 days later, or queued by an earlier version — purge_stale deletes them)`;
608
+ export function formatPendingPurgeLine(n, due, retainDays) {
609
+ return ` Pending purge (idle-marked): ${n} (rows maintenance queued: hidden and still idle 7 days later, or queued by an earlier version — purge_stale deletes those older than its retain window: ${due} at the default ${retainDays} days)`;
604
610
  }
605
611
 
606
612
  /**
@@ -642,3 +648,28 @@ export function formatProbeHints(probe) {
642
648
  if (probe.event?.length > 0) hints.push(`E#${probe.event.join(', E#')} (event)`);
643
649
  return hints;
644
650
  }
651
+
652
+ /**
653
+ * The line under one side of a scan's Needs-review pair (D#461), from reviewRows: type, project,
654
+ * date, the lesson or that there is none, and the start of the narrative — enough to tell two rows
655
+ * with near-identical titles apart without `get`, which counts as an access.
656
+ * @param {{id: number, type?: string, project?: string, created_at_epoch?: number,
657
+ * narrative?: string|null, lesson_learned?: string|null}|undefined} row
658
+ * @returns {string}
659
+ */
660
+ export function formatReviewSide(row) {
661
+ if (!row) return '';
662
+ const flat = (v, n) =>
663
+ truncate(
664
+ String(v ?? '')
665
+ .replace(/\s+/g, ' ')
666
+ .trim(),
667
+ n,
668
+ );
669
+ const lesson =
670
+ row.lesson_learned && row.lesson_learned !== 'none'
671
+ ? `lesson: ${flat(row.lesson_learned, 100)}`
672
+ : 'no lesson';
673
+ const date = row.created_at_epoch ? new Date(row.created_at_epoch).toISOString().slice(0, 10) : '?';
674
+ return `#${row.id} ${row.type || '?'} · ${row.project} · ${date} · ${lesson} · ${flat(row.narrative, 120) || '(no narrative)'}`;
675
+ }
package/cli/fts-check.mjs CHANGED
@@ -3,12 +3,13 @@
3
3
 
4
4
  import { checkFTSIntegrity, rebuildFTS } from '../schema.mjs';
5
5
  import { parseArgs, out, fail } from './common.mjs';
6
+ import { CLI_INVOKE } from '../cli-path.mjs';
6
7
 
7
8
  export function cmdFtsCheck(db, args) {
8
9
  const { positional } = parseArgs(args);
9
10
  const action = positional[0];
10
11
  if (!action) {
11
- fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
12
+ fail(`[mem] Usage: ${CLI_INVOKE} fts-check <check|rebuild>`);
12
13
  return;
13
14
  }
14
15
  if (!['check', 'rebuild'].includes(action)) {
@@ -23,12 +23,12 @@ import {
23
23
  markUndone,
24
24
  } from '../lib/verify-apply-core.mjs';
25
25
  import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
26
- import { shellWord } from '../cli-path.mjs';
26
+ import { shellWord, CLI_INVOKE } from '../cli-path.mjs';
27
27
 
28
28
  const USAGE =
29
- '[mem] Usage: claude-mem-lite verify-apply <proposals.json> [--project P] (dry run)\n' +
30
- ' claude-mem-lite verify-apply <proposals.json> [--project P] --apply --digest <d>\n' +
31
- ' claude-mem-lite verify-apply --undo <backup.json>';
29
+ `[mem] Usage: ${CLI_INVOKE} verify-apply <proposals.json> [--project P] (dry run)\n` +
30
+ ` ${CLI_INVOKE} verify-apply <proposals.json> [--project P] --apply --digest <d>\n` +
31
+ ` ${CLI_INVOKE} verify-apply --undo <backup.json>`;
32
32
 
33
33
  const SNIPPET_CONTEXT = 60;
34
34
  const BACKUP_DIR = join(DB_DIR, 'backups');
package/cli.mjs CHANGED
@@ -338,8 +338,12 @@ if (cmd === '--version' || cmd === '-v' || cmd === '-V' || cmd === 'version') {
338
338
  if (best && bestDist <= 2) {
339
339
  process.stderr.write(`[mem] Did you mean: ${best}?\n`);
340
340
  } else {
341
+ // By path (D#462: the bare name is on PATH only after a global npm install). Imported here,
342
+ // not at the top: this entry takes no static local imports (D#26), and a module it cannot
343
+ // load leaves the old wording rather than no hint.
344
+ const { CLI_INVOKE } = await import('./cli-path.mjs').catch(() => ({ CLI_INVOKE: 'claude-mem-lite' }));
341
345
  process.stderr.write(
342
- '[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n',
346
+ `[mem] Run "${CLI_INVOKE} help" for CLI commands or "${CLI_INVOKE} install" for setup\n`,
343
347
  );
344
348
  }
345
349
  process.exitCode = 1;