claude-mem-lite 3.12.1 → 3.14.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.
package/adopt-content.mjs CHANGED
@@ -1,96 +1,139 @@
1
- // Phase C (Invited-Memory plan, T10): Content generators for the
2
- // claude-mem-lite sentinel line and its companion plugin_claude_mem_lite.md
3
- // detail doc. Kept separate so the strings are testable without side effects
4
- // and can evolve without touching memdir.mjs primitives.
1
+ // CLAUDE.md-steering plan (v3.13): content generators for the claude-mem-lite
2
+ // managed block (written into <cwd>/CLAUDE.md) and its companion
3
+ // <cwd>/.claude/plugin_claude_mem_lite.md detail doc. Kept separate from the
4
+ // claudemd.mjs primitives so the strings are testable without side effects.
5
5
  //
6
- // Bumping CURRENT_SENTINEL_VERSION: pick the next vN and update
7
- // docs/plans/2026-04-16-invited-memory-pattern.md so installs do a version-
8
- // bump replace instead of treating the old content as a user edit.
6
+ // Bumping CURRENT_SENTINEL_VERSION: pick the next vN. On the next SessionStart
7
+ // needsRefresh() sees the version change and refreshes the block in place
8
+ // (version bump = intended content change, so it overwrites rather than treating
9
+ // the drift as a user edit). v1 = legacy memory-dir sentinel (now migrated away);
10
+ // v2 = project-tree CLAUDE.md managed block.
9
11
 
10
12
  import { CLI_INVOKE } from './cli-path.mjs';
11
13
 
12
14
  export const PLUGIN_SLUG = 'claude-mem-lite';
13
- export const CURRENT_SENTINEL_VERSION = 'v1';
15
+ export const CURRENT_SENTINEL_VERSION = 'v2';
14
16
 
15
17
  /**
16
- * One-line entry injected into MEMORY.md's sentinel section. Must stay ≤150
17
- * chars so it clears Claude Code's 200-line MEMORY.md cap with headroom.
18
+ * The concise managed block injected into <cwd>/CLAUDE.md (between the
19
+ * slug-scoped sentinels those are added by claudemd.renderBlock, NOT here).
20
+ * Universal: the memory contract is the same for every project, so there is no
21
+ * per-project-type variation. Keep it tight (cheap always-loaded context); the
22
+ * full tables + rules live in the detail doc this block points to.
18
23
  */
19
- export function getIndexLine() {
20
- return '- [claude-mem-lite](plugin_claude_mem_lite.md) Edit `mem_recall(file=…)`;bugfix `mem_save(type="bugfix", lesson_learned, importance=2)`';
24
+ export function buildClaudeMdBlock() {
25
+ // Intentionally machine-stable: MCP tool names only, NO CLI_INVOKE (that
26
+ // resolves to an absolute path that differs per install — it would make this
27
+ // committed/refreshed block churn across machines). The robust CLI table lives
28
+ // in the detail doc (.claude/, gitignored).
29
+ return `## claude-mem-lite — 持久记忆 (persistent memory)
30
+
31
+ PreToolUse hooks 在 Read/Edit/Write 前已自动 \`mem_recall\` 过往教训。下面是值得你主动发起的调用:
32
+
33
+ | 时机 | 调用 |
34
+ |------|------|
35
+ | Edit/Write 前 | hook 已自动 recall;若注入了 \`#NN\` 教训,下次产出用户可见文字时引用 \`#NN\`(引用=采纳反馈,未引用会衰减) |
36
+ | 解决非平凡 bug 后 | \`mem_save(type="bugfix", lesson_learned="<根因+修法>", importance=2)\` |
37
+ | 非显然架构决策后 | \`mem_save(type="decision", lesson_learned="<约束+取舍>")\` |
38
+ | 推迟到下个会话 | \`mem_defer({title, priority:1|2|3, detail})\`;修好时给 \`mem_save\` 加 \`closes_deferred=[N]\` |
39
+ | 查过往工作 / 历史 | \`mem_search "关键词"\` · \`mem_recent\` · \`mem_timeline\` |
40
+
41
+ 完整工具+CLI 表、citation/decay 规则、save 纪律见 → \`.claude/plugin_claude_mem_lite.md\``;
21
42
  }
22
43
 
23
44
  /**
24
- * Full detail doc rendered into `memdir/plugin_claude_mem_lite.md`. Not
25
- * auto-loaded by Claude Code — the MEMORY.md index line points to it and
26
- * Claude reads it on demand when the injected hint suggests relevance.
45
+ * Full detail doc rendered into `<cwd>/.claude/plugin_claude_mem_lite.md`.
46
+ * Not auto-loaded by Claude Code — the CLAUDE.md block points to it and Claude
47
+ * reads it on demand. claudemd.writeManaged() prepends the `managed-by` marker;
48
+ * this returns pure content.
27
49
  */
28
50
  export function getDetailDoc() {
29
- return `# claude-mem-lite 插件契约
51
+ return `# claude-mem-lite 插件契约(完整)
52
+
53
+ > 由 \`${CLI_INVOKE} adopt\` 生成、随版本自动刷新;卸载用 \`${CLI_INVOKE} unadopt\`。
54
+ > 精炼触发表在项目 \`CLAUDE.md\` 的 \`claude-mem-lite\` 托管块里;本文件是其展开。
55
+ > 设计背景见 docs/CLAUDE-MD-STEERING-PLAN.md。
30
56
 
31
- > \`${CLI_INVOKE} adopt\` 生成;卸载用 \`${CLI_INVOKE} unadopt\`。
32
- > 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
57
+ ## 被动 recall(hook 已自动跑,你只需采纳)
33
58
 
34
- ## 何时调用 MCP 工具
59
+ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该文件:
60
+ - **Read** 路径:asymmetric-quiet——最多 1 条 lesson、120 字符、要求带 \`lesson_learned\`。
61
+ - **Edit / Write** 路径:decision-support——最多 3 条、240 字符、高重要度 bugfix/decision 即使无
62
+ lesson 也注入。
63
+ - Read→Edit 同文件共享 cooldown(不重复注入正文),但 Read 注入后的首个 Edit 会把 lesson **ID**
64
+ 以一行 ack 指令重新浮出。看到 \`#NN [bugfix] …\` 这类行时:**下次产出用户可见文字时引用 \`#NN\`**
65
+ (\`'#NN applied'\` 或 \`'#NN n/a — <理由>'\`)。纯工具回合不算;把 ID 记在工作记忆里,写回时引用。
66
+ - 系统按会话追踪引用:未引用的 lesson 连续 3 个会话后 importance −1(地板 0),被引用的 +1(封顶 3)。
67
+ 引用是给系统的反馈,不是合规仪式——注入池据此自调。
35
68
 
36
- 以下 6 个核心 MCP 工具在 \`tools/list\` 中默认暴露,覆盖了契约的热路径:
37
- \`mem_search\` / \`mem_recent\` / \`mem_recall\` / \`mem_get\` / \`mem_save\` / \`mem_timeline\`。
69
+ ## 何时主动调用 MCP 工具
70
+
71
+ \`tools/list\` 默认暴露 6 个核心工具 + 3 个 defer 工具:
72
+ \`mem_search\` / \`mem_recent\` / \`mem_recall\` / \`mem_get\` / \`mem_save\` / \`mem_timeline\` +
73
+ \`mem_defer\` / \`mem_defer_list\` / \`mem_defer_drop\`。
38
74
 
39
75
  | 时机 | 工具 | 关键参数 |
40
76
  |------|------|----------|
41
- | Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\` —— 过往 bugfix 与教训 |
77
+ | Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\`(hook 通常已代劳) |
42
78
  | Test failure / error | \`mem_search\` | \`query="<错误关键词>", obs_type="bugfix"\` |
43
79
  | Refactor 前 | \`mem_search\` | \`query="<模块>", obs_type="refactor"\` |
44
80
  | 新功能起手 | \`mem_search\` | \`query="<功能区域>"\` —— 找 prior art |
45
- | Bugfix 解决后 | \`mem_save\` | \`type="bugfix", lesson_learned="<根因+修法>", importance=2\` |
46
- | 架构决策后 | \`mem_save\` | \`type="decision", lesson_learned="<取舍理由>", importance=2\` |
81
+ | 解决非平凡 bug | \`mem_save\` | \`type="bugfix", lesson_learned="<根因+修法>", importance=2\` |
82
+ | 非显然架构决策后 | \`mem_save\` | \`type="decision", lesson_learned="<约束+取舍>"\` |
47
83
  | 上下文提到 #NN | \`mem_get\` | \`ids=[NN]\` |
48
84
 
49
- ## Decision rules(替代多步 search)
50
-
51
- - "最近做了啥" \`mem_recent\`
52
- - "<文件> 有哪些记忆" → \`mem_recall\`
53
- - "#NN 前后发生了啥" \`mem_timeline\`
85
+ ## 必做契约(dogfood,本仓库尤其严格)
86
+
87
+ - **解决非平凡 bug 后**(≠ typo / rename)**必须** \`mem_save(type="bugfix",
88
+ lesson_learned="<一行根因+一行修法>", importance=2)\`。判据:未来改同一文件的会话看到这条能否避坑?能→存。
89
+ - **非显然架构决策后**(≠ 改名/挪代码)调 \`mem_save(type="decision",
90
+ lesson_learned="<约束+为何这样选+牺牲了什么>")\`。\`decision\` 命中率显著高于 \`change\`(当前遥测约
91
+ 3:1,会漂移——用 \`${CLI_INVOKE} stats\` 实测,别套固定倍数);方向稳健:一条好 decision 抵数条 change。
92
+ 别注水:decision 只留给真权衡,不是风格选择。
93
+ - **推迟到未来会话**(≠ 在途 todo、≠ 本 PR 跟进)调
94
+ \`mem_defer({title, priority:1|2|3, detail:"<约束+为何推迟>"})\`。
95
+ 触发词:中文「下次/下个会话/不在本轮范围/留给下个会话」;en「next session / defer to next round /
96
+ out of scope for this PR / pick up later」。
97
+ - 修掉 deferred 项时 **必须** 给 \`mem_save\` 加 \`closes_deferred=[N]\`(N 是 SessionStart
98
+ \`### Deferred Work\` banner 里的序号,或原始 id \`["D#42"]\`,混用 OK),让 carry-forward 链闭合。
99
+ 若该项无需修(flaky/scope shift)改用 \`mem_defer_drop({id, reason})\`,reason 必填、作审计。
100
+ - **不要为凑 schema 写 \`lesson_learned: 'none'\`**:写不出能复用的教训就留 NULL,接受低重要度观测。
101
+ Haiku 默认过于激进地填 "none"——手动 save 时覆盖它。
54
102
 
55
103
  ## 维护 / 管理类工具(走 CLI)
56
104
 
57
- v2.34.0 起,以下 11 个工具从 \`tools/list\` 中隐藏以缩小启动上下文;它们仍注册在
58
- MCP 层,按名 \`tools/call\` 仍可命中,但对 Claude Code 这类只读 tools/list
59
- 调用方只走下面的 CLI 入口:
105
+ 以下工具从 \`tools/list\` 隐藏(缩小启动上下文);仍注册在 MCP 层、按名 \`tools/call\` 可命中,
106
+ 但对 Claude Code 这类只读 tools/list 的调用方只走 CLI:
60
107
 
61
108
  | 场景 | CLI |
62
109
  |------|-----|
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\` 改阈值) |
110
+ | 清理过期记忆 | \`${CLI_INVOKE} maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale --confirm\`(删行必须 \`--confirm\`) |
111
+ | 深度优化(Haiku) | \`${CLI_INVOKE} optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\`) |
112
+ | 压缩旧条目 | \`${CLI_INVOKE} compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\`) |
66
113
  | FTS5 索引检查 / 重建 | \`${CLI_INVOKE} fts-check [--rebuild]\` |
67
114
  | tier 分组浏览 | \`${CLI_INVOKE} browse [--tier active]\` |
68
115
  | 导出 JSON/JSONL | \`${CLI_INVOKE} export [--format jsonl]\` |
69
116
  | 统计总量 / 健康 | \`${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>\` |
73
- | 按 registry 名载入 skill/agent | (MCP only:\`mem_use\`;由用户主动请求时才使用) |
117
+ | 删除 / 更新某条 | \`${CLI_INVOKE} delete <id>[,<id>]\` · \`${CLI_INVOKE} update <id> [--title ...]\` |
118
+ | skill-agent registry | \`${CLI_INVOKE} registry <list\\|search\\|import>\` |
74
119
 
75
120
  ## CLI 速查(常用检索)
76
121
 
77
122
  | 命令 | 用途 |
78
123
  |------|------|
79
- | \`${CLI_INVOKE} search "query"\` | FTS5 全文搜索 |
124
+ | \`${CLI_INVOKE} search "query"\` | FTS5 全文搜索(默认排除低信号 \`Modified X\` 等;加 \`--include-noise\` 找文件变更记录) |
80
125
  | \`${CLI_INVOKE} search "err" --type bugfix\` | 按类型过滤 |
81
126
  | \`${CLI_INVOKE} recall "file.mjs"\` | 文件相关记忆 |
82
127
  | \`${CLI_INVOKE} recent 5\` | 最近 5 条 |
83
128
  | \`${CLI_INVOKE} get 42,43\` | 按 ID 展开 |
84
129
  | \`${CLI_INVOKE} timeline --anchor 42\` | 时间线上下文 |
85
130
 
86
- ## 质量门槛
87
-
88
- - \`mem_save\` 的 \`lesson_learned\` 不要写 \`none\`——写不出教训就保持 NULL
89
- - \`decision\` 的命中率高于 \`change\`(当前遥测约 3:1,数值会漂移——用 \`${CLI_INVOKE} stats\` 实测,别套固定倍数);方向稳健:一条好 decision 抵数条 change
90
- - 一般搜索跳过 \`obs_type\` 让系统自动路由;特定意图再过滤
91
-
92
- ## 卸载
131
+ ## 卸载 / 关闭
93
132
 
94
- \`${CLI_INVOKE} unadopt\` 精确移除 sentinel + 本文件;其它 MEMORY.md 内容不动。
133
+ - \`${CLI_INVOKE} unadopt\`:移除 CLAUDE.md 托管块 + \`.claude/plugin_claude_mem_lite.md\`;
134
+ CLAUDE.md 里你自己的内容(sentinel 之外)不动。
135
+ - 本项目永久关闭自动 adopt:\`${CLI_INVOKE} adopt --disable\`(\`--enable\` 重新武装)。
136
+ - 全局禁用自动 adopt:环境变量 \`MEM_NO_AUTO_ADOPT=1\`。
137
+ - 关闭版本漂移自动刷新(保留你对托管块的手改):\`CLAUDE_MEM_NO_TEMPLATE_REFRESH=1\`。
95
138
  `;
96
139
  }
package/bash-utils.mjs CHANGED
@@ -76,7 +76,14 @@ export function detectBashSignificance(input, response) {
76
76
  // Allow intervening global git options (`-C <path>`, `-c k=v`, `--no-pager`, …) between
77
77
  // `git` and the subcommand — `git -C /repo push` is the standard multi-repo/scripted form.
78
78
  const isGit = /\bgit\s+(?:(?:-[cC]\s+\S+|--?[\w-]+(?:=\S+)?)\s+)*(commit|merge|rebase|cherry-pick|push)\b/i.test(cmd);
79
- const isDeploy = /\b(deploy|docker|kubectl|terraform)\b/i.test(cmd);
79
+ // Deploy + publish/release: the actual "ship". Package publish and GitHub
80
+ // release are rare, high-value events; without them a release session records
81
+ // the git push but not the publish that defines it. `npm run publish-*` is
82
+ // excluded (custom script, ambiguous) — only the direct publish verb counts.
83
+ const isDeploy = /\b(deploy|docker|kubectl|terraform)\b/i.test(cmd)
84
+ || /\b(?:npm|pnpm|yarn|bun|cargo)\s+publish\b/i.test(cmd)
85
+ || /\bgh\s+release\s+(?:create|edit|upload|delete)\b/i.test(cmd)
86
+ || /\btwine\s+upload\b/i.test(cmd);
80
87
  return {
81
88
  isError, isTest, isBuild, isGit, isDeploy,
82
89
  isSignificant: isError || isTest || isBuild || isGit || isDeploy,
package/claudemd.mjs ADDED
@@ -0,0 +1,244 @@
1
+ // CLAUDE.md-steering plan (v3.13): claudemd.mjs — primitives for the
2
+ // project-tree managed block at <cwd>/CLAUDE.md plus an on-demand detail doc
3
+ // at <cwd>/.claude/plugin_<slug>.md.
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
8
+ // index meant for the user's own memories. This module mirrors the design
9
+ // shipped by the sibling code-graph-mcp plugin (claude-plugin/scripts/adopt.js)
10
+ // and the oh-my-claudecode versioned `<!-- :begin vN -->` managed-block pattern.
11
+ //
12
+ // The managed block is plugin-owned: it auto-refreshes when the shipped content
13
+ // drifts (version bump or template change), UNLESS CLAUDE_MEM_NO_TEMPLATE_REFRESH=1.
14
+ // User prose OUTSIDE the slug-scoped sentinel is never touched. The slug scope is
15
+ // what keeps this independent from code-graph-mcp's own block in the same file.
16
+ //
17
+ // See docs/CLAUDE-MD-STEERING-PLAN.md for rationale + migration.
18
+
19
+ import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, mkdirSync, rmdirSync, readdirSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { createHash } from 'crypto';
22
+ import { memdirPath, removePluginSection, removePluginDoc, isAdopted as memdirIsAdopted } from './memdir.mjs';
23
+
24
+ // ─── Path helpers ────────────────────────────────────────────────────────────
25
+
26
+ function slugSnake(slug) { return String(slug).replace(/[^a-zA-Z0-9]/g, '_'); }
27
+
28
+ export function claudeMdPath(cwd) { return join(cwd, 'CLAUDE.md'); }
29
+ function dotClaudeDir(cwd) { return join(cwd, '.claude'); }
30
+ export function detailDocPath(cwd, slug) { return join(dotClaudeDir(cwd), `plugin_${slugSnake(slug)}.md`); }
31
+ function stateFilePath(cwd, slug) { return join(dotClaudeDir(cwd), `.plugin_${slugSnake(slug)}_state.json`); }
32
+
33
+ // First line of the detail doc — an invisible (in rendered markdown) marker that
34
+ // lets us distinguish our generated copy from a user's same-named file.
35
+ function managedByMarker(slug) { return `<!-- managed-by: ${slug} -->`; }
36
+
37
+ // ─── Sentinel rendering & parsing ────────────────────────────────────────────
38
+
39
+ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
40
+
41
+ // Slug-scoped so stripping our block never disturbs another plugin's block
42
+ // (e.g. code-graph-mcp's `<!-- code-graph-mcp:begin -->`) sitting in the same
43
+ // CLAUDE.md. Matches any version vN+ for migration/idempotency. The separators
44
+ // are `\r?\n` (not bare `\n`) so a CLAUDE.md re-saved with Windows CRLF endings
45
+ // still matches — otherwise the block read as "absent" and a fresh LF copy got
46
+ // appended every SessionStart, growing the file without bound (review C1/H2).
47
+ function blockBody(esc) { return `<!-- ${esc}:begin (v\\d+) -->\\r?\\n([\\s\\S]*?)\\r?\\n<!-- ${esc}:end -->`; }
48
+ function blockRegex(slug) { return new RegExp(blockBody(escapeRe(slug))); }
49
+ function blockRegexG(slug) { return new RegExp(blockBody(escapeRe(slug)), 'g'); }
50
+
51
+ function renderBlock(slug, version, body) {
52
+ return `<!-- ${slug}:begin ${version} -->\n${body}\n<!-- ${slug}:end -->`;
53
+ }
54
+
55
+ function sha256(s) { return createHash('sha256').update(s).digest('hex'); }
56
+
57
+ function atomicWrite(path, content) {
58
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
59
+ writeFileSync(tmp, content);
60
+ renameSync(tmp, path);
61
+ }
62
+
63
+ function writeState(cwd, slug, state) {
64
+ const dir = dotClaudeDir(cwd);
65
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
66
+ atomicWrite(stateFilePath(cwd, slug), JSON.stringify(state, null, 2) + '\n');
67
+ }
68
+
69
+ function clearState(cwd, slug) {
70
+ const p = stateFilePath(cwd, slug);
71
+ if (existsSync(p)) try { unlinkSync(p); } catch { /* best-effort */ }
72
+ }
73
+
74
+ // ─── Public API ──────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Parse <cwd>/CLAUDE.md for our slug-scoped managed block.
78
+ * @returns {{ exists: boolean, version: string|null, body: string|null, raw: string }}
79
+ */
80
+ export function readBlock(cwd, slug) {
81
+ const p = claudeMdPath(cwd);
82
+ if (!existsSync(p)) return { exists: false, version: null, body: null, raw: '' };
83
+ const raw = readFileSync(p, 'utf8');
84
+ const m = raw.match(blockRegex(slug));
85
+ if (!m) return { exists: true, version: null, body: null, raw };
86
+ // Normalize CRLF in the captured body so drift comparison (needsRefresh) is
87
+ // line-ending-agnostic — a CRLF-saved file matching the shipped LF content
88
+ // must NOT be seen as drifted and rewritten every session.
89
+ return { exists: true, version: m[1], body: m[2].replace(/\r\n/g, '\n'), raw };
90
+ }
91
+
92
+ /**
93
+ * Adopted under the new scheme = managed block present in CLAUDE.md AND the
94
+ * detail doc exists. Both must hold so a half-written state still self-heals.
95
+ */
96
+ export function isAdopted(cwd, slug) {
97
+ const blk = readBlock(cwd, slug);
98
+ return blk.body !== null && existsSync(detailDocPath(cwd, slug));
99
+ }
100
+
101
+ /**
102
+ * Whether the installed block/doc has drifted from the shipped content — i.e.
103
+ * a version bump or a template edit means we should refresh. Returns true when
104
+ * the block is missing, the version differs, the block body differs, or the
105
+ * detail doc is missing / differs. The block is plugin-managed: drift is
106
+ * overwritten on refresh (opt out with CLAUDE_MEM_NO_TEMPLATE_REFRESH=1 at the
107
+ * caller). User content lives OUTSIDE the sentinel and is never compared.
108
+ */
109
+ export function needsRefresh(cwd, { slug, version, block, doc }) {
110
+ const blk = readBlock(cwd, slug);
111
+ if (blk.body === null) return true;
112
+ if (blk.version !== version) return true;
113
+ if (blk.body !== block) return true;
114
+ const dp = detailDocPath(cwd, slug);
115
+ if (!existsSync(dp)) return true;
116
+ let cur;
117
+ try { cur = readFileSync(dp, 'utf8'); } catch { return true; }
118
+ return cur !== `${managedByMarker(slug)}\n${doc}`;
119
+ }
120
+
121
+ /**
122
+ * Write (insert-or-replace) the managed block in CLAUDE.md and the detail doc
123
+ * under .claude/, plus a state sidecar. Idempotent on the user-visible files:
124
+ * re-running with identical inputs leaves CLAUDE.md and the detail doc byte-for-
125
+ * byte unchanged (only the state sidecar's writtenAt updates).
126
+ *
127
+ * CLAUDE.md is created if absent. Only our slug-scoped region is rewritten;
128
+ * everything else in the file is preserved verbatim.
129
+ *
130
+ * @returns {{action: 'created'|'updated'|'unchanged'}} (block disposition)
131
+ */
132
+ export function writeManaged(cwd, { slug, version, block, doc }) {
133
+ const p = claudeMdPath(cwd);
134
+ const raw = existsSync(p) ? readFileSync(p, 'utf8') : '';
135
+ const section = renderBlock(slug, version, block);
136
+ const m = raw.match(blockRegex(slug));
137
+
138
+ let next, action;
139
+ if (!m) {
140
+ if (raw.length === 0) next = section + '\n';
141
+ else if (raw.endsWith('\n\n')) next = raw + section + '\n';
142
+ else if (raw.endsWith('\n')) next = raw + '\n' + section + '\n';
143
+ else next = raw + '\n\n' + section + '\n';
144
+ action = 'created';
145
+ } else {
146
+ next = raw.replace(m[0], section);
147
+ action = next !== raw ? 'updated' : 'unchanged';
148
+ }
149
+ // H2: collapse any DUPLICATE same-slug blocks (keep the first, drop the rest).
150
+ // Defends against a CRLF-orphaned copy a pre-fix build may have appended, or a
151
+ // user paste — otherwise the extras would be invisible to refresh/unadopt.
152
+ let seen = 0;
153
+ const deduped = next.replace(blockRegexG(slug), (whole) => (seen++ === 0 ? whole : ''));
154
+ if (deduped !== next) {
155
+ next = deduped.replace(/\n{3,}/g, '\n\n');
156
+ if (action === 'unchanged') action = 'updated';
157
+ }
158
+ if (next !== raw) atomicWrite(p, next);
159
+
160
+ // Detail doc (marker on first line so unadopt/refresh can tell it apart from a
161
+ // user's same-named file).
162
+ const docContent = `${managedByMarker(slug)}\n${doc}`;
163
+ const dp = detailDocPath(cwd, slug);
164
+ const dir = dotClaudeDir(cwd);
165
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
166
+ const existingDoc = existsSync(dp) ? readFileSync(dp, 'utf8') : null;
167
+ if (existingDoc !== docContent) atomicWrite(dp, docContent);
168
+
169
+ writeState(cwd, slug, {
170
+ version,
171
+ blockHash: sha256(block),
172
+ docHash: sha256(doc),
173
+ writtenAt: new Date().toISOString(),
174
+ });
175
+ return { action };
176
+ }
177
+
178
+ /**
179
+ * Remove our managed block from CLAUDE.md (preserving all other content) and
180
+ * delete the detail doc + state sidecar. Best-effort removes an emptied
181
+ * .claude/ directory.
182
+ * @returns {{action: 'removed'|'absent'}}
183
+ */
184
+ export function removeManaged(cwd, slug) {
185
+ const p = claudeMdPath(cwd);
186
+ let action = 'absent';
187
+ if (existsSync(p)) {
188
+ let raw = readFileSync(p, 'utf8');
189
+ // H2: loop so ALL same-slug blocks are removed, not just the first (a
190
+ // duplicate/CRLF-orphaned copy must not survive unadopt).
191
+ let m;
192
+ while ((m = raw.match(blockRegex(slug)))) {
193
+ const blockAtStart = m.index === 0;
194
+ let start = m.index;
195
+ let end = m.index + m[0].length;
196
+ if (raw[end] === '\n') end++;
197
+ if (start > 0 && raw.slice(0, start).endsWith('\n\n')) start--;
198
+ raw = raw.slice(0, start) + raw.slice(end);
199
+ raw = raw.replace(/\n{3,}/g, '\n\n');
200
+ if (blockAtStart) raw = raw.replace(/^\s+/, '');
201
+ action = 'removed';
202
+ }
203
+ if (action === 'removed') atomicWrite(p, raw);
204
+ }
205
+ const dp = detailDocPath(cwd, slug);
206
+ if (existsSync(dp)) try { unlinkSync(dp); } catch { /* best-effort */ }
207
+ clearState(cwd, slug);
208
+ // Drop an emptied .claude/ so unadopt leaves no trace (skips if it holds
209
+ // anything else — e.g. settings.local.json).
210
+ try {
211
+ const dir = dotClaudeDir(cwd);
212
+ if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
213
+ } catch { /* best-effort */ }
214
+ return { action };
215
+ }
216
+
217
+ // ─── Legacy migration ────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * One-time (idempotent) cleanup of the pre-v3.13 scheme: strip the slug-scoped
221
+ * sentinel from the project's memory-dir MEMORY.md and delete the memory-dir
222
+ * detail doc + state sidecar. Slug-scoped, so an adjacent code-graph-mcp block
223
+ * and all user prose survive byte-intact. Respects the foreign-content guard
224
+ * (a sentinel with no state sidecar is left in place) unless force=true.
225
+ *
226
+ * Absent legacy artifacts → harmless no-op, so this is safe to call every
227
+ * SessionStart.
228
+ *
229
+ * @returns {{action: 'removed'|'absent'|'skipped-foreign'}}
230
+ */
231
+ export function migrateLegacyMemoryDir(cwd, slug, { force = false } = {}) {
232
+ const memdir = memdirPath(cwd);
233
+ const r = removePluginSection(memdir, slug, { force });
234
+ // M3: only delete the legacy detail doc when we actually removed OUR sentinel.
235
+ // On 'skipped-foreign' the sentinel is left in place (not provably plugin-
236
+ // written), so deleting its companion doc would leave a dangling pointer.
237
+ if (r.action === 'removed') removePluginDoc(memdir, slug);
238
+ return r;
239
+ }
240
+
241
+ /** True if the legacy memory-dir sentinel still exists for this project. */
242
+ export function hasLegacyMemdirSentinel(cwd, slug) {
243
+ return memdirIsAdopted(memdirPath(cwd), slug);
244
+ }
package/commands/adopt.md CHANGED
@@ -1,60 +1,67 @@
1
1
  ---
2
2
  name: adopt
3
- description: "Use when: user asks to increase claude-mem-lite's tool-invocation rate in the current project, or wants to install the invited-memory sentinel so Claude Code auto-loads the contract as user-memory. Writes a single sentinel-wrapped line to ~/.claude/projects/<encoded>/memory/MEMORY.md plus a plugin_claude_mem_lite.md detail file. Run /unadopt to remove."
3
+ description: "Use when: user asks to increase claude-mem-lite's tool-invocation rate in the current project, or to (re)install the steering block. Writes a sentinel-wrapped managed block into <cwd>/CLAUDE.md plus a <cwd>/.claude/plugin_claude_mem_lite.md detail doc, and migrates away any legacy memory-dir sentinel. Runs automatically on SessionStart; use this to force it now. Run /unadopt to remove."
4
4
  ---
5
5
 
6
6
  # /adopt
7
7
 
8
- Install the **invited-memory** sentinel into the current project's Claude Code
9
- memdir (`~/.claude/projects/<encoded>/memory/`). The sentinel line carries the
10
- MCP-tool triggers (`mem_recall` / `mem_save`) at system-prompt authority
11
- (framed as "user's auto-memory") so Claude Code is more likely to call them
12
- proactively than when those same triggers live in MCP server instructions.
8
+ Install the claude-mem-lite **steering block** into the current project's
9
+ `CLAUDE.md` (the canonical home for project instructions — loaded by Claude Code
10
+ at system-prompt authority). This replaces the pre-v3.13 scheme that seeded the
11
+ project's memory-dir `MEMORY.md`; that polluted an index meant for the user's own
12
+ memories, and `MEMORY.md` carries no more weight than `CLAUDE.md` anyway.
13
+
14
+ This normally runs automatically on every SessionStart — invoke `/adopt` only to
15
+ force it immediately (e.g. after editing the block out by hand).
13
16
 
14
17
  ## What it writes
15
18
 
16
- 1. **`MEMORY.md`** — ONE sentinel-wrapped line under a `## 插件契约` header.
17
- Idempotent: re-running replaces the block only if the version bumped.
18
- 2. **`plugin_claude_mem_lite.md`** Detailed contract (CLI cheatsheet +
19
- MCP decision rules). Not auto-loaded; Claude reads it on demand when the
20
- MEMORY.md pointer surfaces in context.
21
- 3. **`.plugin_claude_mem_lite_state.json`** Sidecar with sentinel body hash
22
- for user-edit detection.
19
+ 1. **`<cwd>/CLAUDE.md`** — a concise `<!-- claude-mem-lite:begin v2 -->…<!-- :end -->`
20
+ managed block (trigger table `mem_recall` / `mem_save` / `mem_defer`).
21
+ Slug-scoped: only this block is managed; the rest of your `CLAUDE.md` is
22
+ preserved verbatim. Auto-refreshes when the shipped content drifts.
23
+ 2. **`<cwd>/.claude/plugin_claude_mem_lite.md`** the full contract (tool tables,
24
+ CLI cheatsheet, citation/decay + save discipline). First line is a
25
+ `<!-- managed-by: claude-mem-lite -->` marker. Not auto-loaded; the CLAUDE.md
26
+ block points to it and Claude reads it on demand.
27
+ 3. **`<cwd>/.claude/.plugin_claude_mem_lite_state.json`** — drift-tracking sidecar.
28
+
29
+ It also **migrates** this project's legacy memory-dir sentinel away (strips the
30
+ `claude-mem-lite:*` block from `MEMORY.md` and deletes the old memory-dir detail
31
+ doc). Other plugins' blocks (e.g. `code-graph-mcp:*`) and your own prose survive.
23
32
 
24
33
  ## Flags
25
34
 
26
- - `--force` — overwrite a sentinel block that was manually edited
35
+ - `--force` — also force-clean a legacy memory-dir block lacking a state sidecar
27
36
  - `--dry-run` — print intended writes without touching disk
28
- - `--all` — adopt every memdir under `~/.claude/projects/*/memory/`
29
- - `--status` list all adopted projects with their sentinel versions
37
+ - `--all` — legacy-cleanup sweep: strip the old memory-dir sentinel across **every**
38
+ project at once. (It does NOT write CLAUDE.md blocks for other projects their
39
+ real paths can't be recovered from the encoded memdir slug; those adopt
40
+ per-project on each one's next SessionStart.)
41
+ - `--status` — show this project's adoption + count of memdirs awaiting migration
42
+ - `--disable` / `--enable` — per-project opt-out of automatic SessionStart adopt
30
43
 
31
- ## Removal
44
+ ## Removal & opt-out
32
45
 
33
- `/unadopt` precisely removes the sentinel block + plugin doc + state file.
34
- User content outside the sentinel is preserved.
46
+ - `/unadopt` removes the CLAUDE.md block + `.claude/` detail doc (your prose stays).
47
+ - `claude-mem-lite adopt --disable` permanently stops auto-adopt for this project.
48
+ - `MEM_NO_AUTO_ADOPT=1` disables auto-adopt globally.
49
+ - `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` freezes the block against drift-refresh
50
+ (keeps your hand-edits to the managed block).
35
51
 
36
52
  ## Conservative layer still active
37
53
 
38
54
  Adoption does NOT remove the hook-based injection (SessionStart context,
39
- UserPromptSubmit related-memory). Those remain as fallback for older Claude
40
- Code versions. Post-adopt the MCP `WHEN TO USE` section + the `File Lessons` /
41
- `Key Context` sections auto-trim since the sentinel line already carries the
42
- triggers at higher authority.
43
-
44
- ## Restart required for MCP trim to take effect
45
-
46
- The MCP server builds its instructions once at server boot and the protocol
47
- has no way to push updated instructions to an already-connected Claude Code
48
- session. After running `adopt`:
55
+ UserPromptSubmit related-memory). Those remain as fallback. Post-adopt the MCP
56
+ `WHEN TO USE` section + the `File Lessons` / `Key Context` sections auto-trim,
57
+ since the CLAUDE.md block already carries the triggers at higher authority.
49
58
 
50
- - The **MEMORY.md sentinel** appears on the **next SessionStart** automatically.
51
- - Hook-layer trim (`File Lessons` / `Key Context` / lesson suffix) applies
52
- on the next SessionStart.
53
- - MCP-instructions trim (`WHEN TO USE` / `Decision rules` sections) only
54
- takes effect after Claude Code itself restarts (or at least re-attaches
55
- the mem-lite MCP server). If you still see the verbose MCP instructions after
56
- adopt, a `/exit` + fresh session is enough.
59
+ ## Restart caveat for the MCP-instructions trim
57
60
 
61
+ The CLAUDE.md block + hook-layer trim apply on the **next SessionStart**. The MCP
62
+ server builds its instructions once at boot, so the MCP-instructions trim
63
+ (`WHEN TO USE` / `Decision rules`) only takes effect after Claude Code restarts
64
+ (or re-attaches the mem-lite MCP server). A `/exit` + fresh session is enough.
58
65
  Same caveat applies in reverse for `/unadopt`.
59
66
 
60
67
  !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs adopt $ARGUMENTS
@@ -1,30 +1,44 @@
1
1
  ---
2
2
  name: unadopt
3
- description: "Use when: user wants to remove the invited-memory sentinel from the current project (or all projects with --all). Cleans MEMORY.md sentinel block + plugin_claude_mem_lite.md + state sidecar. User content outside the sentinel is preserved. Benign no-op on never-adopted memdirs."
3
+ description: "Use when: user wants to remove the claude-mem-lite steering block from the current project (or sweep legacy memory-dir sentinels everywhere with --all). Removes the <cwd>/CLAUDE.md managed block + <cwd>/.claude/plugin_claude_mem_lite.md and cleans any legacy memory-dir residue. User content outside the sentinel is preserved. Benign no-op when not adopted."
4
4
  ---
5
5
 
6
6
  # /unadopt
7
7
 
8
- Remove the claude-mem-lite invited-memory sentinel from the current project's
9
- Claude Code memdir. Opposite of `/adopt`.
8
+ Remove the claude-mem-lite steering block from the current project. Opposite of
9
+ `/adopt`.
10
10
 
11
11
  ## What it removes
12
12
 
13
- 1. The `<!-- claude-mem-lite:begin vN --> ... <!-- claude-mem-lite:end -->`
14
- block from `MEMORY.md` (plus the preceding `## 插件契约` header line it owns).
15
- 2. `plugin_claude_mem_lite.md` detail file.
16
- 3. `.plugin_claude_mem_lite_state.json` sidecar.
13
+ 1. The `<!-- claude-mem-lite:begin vN --> <!-- claude-mem-lite:end -->` managed
14
+ block from `<cwd>/CLAUDE.md`. Everything else in the file is preserved
15
+ byte-for-byte.
16
+ 2. `<cwd>/.claude/plugin_claude_mem_lite.md` detail doc.
17
+ 3. `<cwd>/.claude/.plugin_claude_mem_lite_state.json` sidecar (and an emptied
18
+ `.claude/` dir).
17
19
 
18
- Everything else in `MEMORY.md` is preserved byte-for-byte.
20
+ It also cleans any leftover **legacy** memory-dir sentinel + detail doc for this
21
+ project (slug-scoped — other plugins' blocks survive).
19
22
 
20
23
  ## Flags
21
24
 
22
- - `--all` — unadopt every memdir under `~/.claude/projects/*/memory/`
25
+ - `--force` — also remove a legacy memory-dir block lacking a state sidecar
26
+ - `--dry-run` — preview what would be removed; no writes
27
+ - `--all` — legacy-cleanup sweep: strip the old memory-dir sentinel across every
28
+ project (CLAUDE.md blocks for other projects can't be located from the encoded
29
+ slug — `cd` into a project and run `/unadopt` there to remove its block).
30
+ - `--status` — read-only adoption probe (mirrors `/adopt --status`)
31
+
32
+ ## Note: this does not stop auto-adopt
33
+
34
+ `/unadopt` removes the block now, but the next SessionStart re-adopts unless you
35
+ also disable it: `claude-mem-lite adopt --disable` (per-project) or
36
+ `MEM_NO_AUTO_ADOPT=1` (global).
23
37
 
24
38
  ## Aftermath
25
39
 
26
40
  Once unadopted, the conservative hook layer (SessionStart `File Lessons` /
27
- `Key Context`, MCP instructions `WHEN TO USE`) goes back to verbose mode
28
- Claude Code will see the full injection again on the next session start.
41
+ `Key Context`, MCP instructions `WHEN TO USE`) returns to verbose mode on the
42
+ next session start.
29
43
 
30
44
  !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS