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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +150 -185
- package/adopt-content.mjs +91 -48
- package/bash-utils.mjs +8 -1
- package/claudemd.mjs +244 -0
- package/commands/adopt.md +43 -36
- package/commands/unadopt.md +25 -11
- package/hook-memory.mjs +26 -3
- package/hook-shared.mjs +13 -7
- package/hook.mjs +27 -26
- package/lib/startup-dashboard.mjs +1 -1
- package/lib/upgrade-banner.mjs +18 -7
- package/nlp.mjs +33 -6
- package/package.json +2 -1
- package/server.mjs +2 -2
- package/source-files.mjs +2 -0
- package/tier.mjs +6 -1
package/adopt-content.mjs
CHANGED
|
@@ -1,96 +1,139 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// detail doc. Kept separate
|
|
4
|
-
//
|
|
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
|
|
7
|
-
//
|
|
8
|
-
// bump
|
|
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 = '
|
|
15
|
+
export const CURRENT_SENTINEL_VERSION = 'v2';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
20
|
-
|
|
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
|
|
25
|
-
* auto-loaded by Claude Code — the
|
|
26
|
-
*
|
|
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
|
-
|
|
32
|
-
> 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
|
|
57
|
+
## 被动 recall(hook 已自动跑,你只需采纳)
|
|
33
58
|
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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="<路径>"
|
|
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
|
-
|
|
|
46
|
-
|
|
|
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
|
-
##
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
|
71
|
-
|
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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` —
|
|
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` —
|
|
29
|
-
|
|
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`
|
|
34
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
package/commands/unadopt.md
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unadopt
|
|
3
|
-
description: "Use when: user wants to remove the
|
|
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
|
|
9
|
-
|
|
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 -->
|
|
14
|
-
block from
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
- `--
|
|
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`)
|
|
28
|
-
|
|
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
|