claude-mem-lite 2.31.2 → 2.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +50 -0
- package/README.zh-CN.md +46 -0
- package/adopt-cli.mjs +166 -0
- package/adopt-content.mjs +75 -0
- package/commands/adopt.md +44 -0
- package/commands/unadopt.md +30 -0
- package/hook-context.mjs +12 -5
- package/hook-shared.mjs +31 -0
- package/install.mjs +27 -0
- package/mem-cli.mjs +17 -0
- package/memdir.mjs +252 -0
- package/package.json +9 -1
- package/scripts/user-prompt-search.js +5 -1
- package/server-internals.mjs +48 -0
- package/server.mjs +3 -36
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ The original sends **everything to the LLM and hopes it filters well**. claude-m
|
|
|
92
92
|
- **Prompt-time memory injection** -- UserPromptSubmit hook automatically searches and injects relevant past observations with recency and importance weighting
|
|
93
93
|
- **Smart skill invocation** -- Auto-loaded and searched managed skills/agents include portable `~` paths with `Read()` guidance; native plugin skills recommend `Skill("full:name")`; prevents `Skill()` misuse for managed resources that aren't registered with Claude Code's native handler
|
|
94
94
|
- **Dual injection dedup** -- `user-prompt-search.js` and `handleUserPrompt` coordinate via temp file to prevent duplicate memory injection
|
|
95
|
+
- **Plugin cache hook self-heal** -- Claude Code runtime reads plugin hooks from `~/.claude/plugins/cache/<mp>/<plugin>/<ver>/hooks/hooks.json`, not from the marketplace source. When `install.mjs`-managed `settings.json` hooks coexist with a stale cache `hooks.json` (e.g. from a previous marketplace install or a plugin auto-update), the runtime registers hooks twice → every session start / user prompt fires twice. `install.mjs` and `hook-update.mjs` now clear cache `hooks.json` in every version dir, and `hook.mjs session-start` self-heals on every session (gated by `hasInstallManagedHooks` so plugin-only users are not affected). `install.mjs status` reports cache pollution state (since v2.31.1/2.31.2).
|
|
95
96
|
- **Result-dedup cooldown** -- User-prompt memory injection uses result-overlap detection (>80% ID overlap → skip) instead of time-based cooldown, allowing topic switches within seconds while preventing redundant injections
|
|
96
97
|
- **OR query fallback** -- When AND-joined FTS5 queries return zero results, automatically relaxes to OR-joined queries for broader recall (applied in both user-prompt-search and hook-memory paths)
|
|
97
98
|
- **Configurable LLM model** -- Switch between Haiku (fast/cheap) and Sonnet (deeper analysis) via `CLAUDE_MEM_MODEL` env var
|
|
@@ -101,6 +102,9 @@ The original sends **everything to the LLM and hopes it filters well**. claude-m
|
|
|
101
102
|
- **LLM concurrency control** -- File-based semaphore limits background workers to 2 concurrent LLM calls, preventing resource contention
|
|
102
103
|
- **stdin overflow protection** -- Hook input truncated at 256KB with regex-based action salvage for oversized tool outputs
|
|
103
104
|
- **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/clear` or `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap
|
|
105
|
+
- **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
|
|
106
|
+
- **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`
|
|
107
|
+
- **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`. Slash commands: `/lesson`, `/bug`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic
|
|
104
108
|
- **In-place observation updates** -- `mem_update` tool modifies existing observations atomically (field update + FTS text rebuild + vector re-computation in one transaction), preserving original IDs and references
|
|
105
109
|
- **Bulk export** -- `mem_export` tool exports observations as JSON or JSONL, with project/type/date filtering and 1000-row pagination cap with batch guidance
|
|
106
110
|
- **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
|
|
@@ -235,6 +239,8 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
|
|
|
235
239
|
/mem timeline <query> # Browse timeline around a match
|
|
236
240
|
/mem browse # Tier-grouped memory dashboard
|
|
237
241
|
/mem <query> # Shorthand for search
|
|
242
|
+
/lesson <text> # Save a non-obvious lesson to the events table (v2.31.0)
|
|
243
|
+
/bug <text> # Log a known bug + repro steps to the events table (v2.31.0)
|
|
238
244
|
```
|
|
239
245
|
|
|
240
246
|
### Efficient Search Workflow
|
|
@@ -245,6 +251,48 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
|
|
|
245
251
|
3. mem_get(ids=[12345, 12346]) -> full details
|
|
246
252
|
```
|
|
247
253
|
|
|
254
|
+
### Invited Memory (v2.32+)
|
|
255
|
+
|
|
256
|
+
Opt-in mechanism that installs a single sentinel-wrapped line into the project's
|
|
257
|
+
memdir (`~/.claude/projects/<encoded>/memory/MEMORY.md`) so Claude Code loads
|
|
258
|
+
the plugin's MCP-tool triggers as **user-memory** — a higher instruction-following
|
|
259
|
+
authority than MCP server instructions (which are framed as tool metadata).
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
claude-mem-lite adopt # install for current project
|
|
263
|
+
claude-mem-lite adopt --all # install for every project under ~/.claude/projects/
|
|
264
|
+
claude-mem-lite adopt --status # list adopted projects + sentinel versions
|
|
265
|
+
claude-mem-lite adopt --dry-run # preview without writing
|
|
266
|
+
claude-mem-lite unadopt # remove cleanly
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Slash commands `/adopt` and `/unadopt` wrap the same CLI.
|
|
270
|
+
|
|
271
|
+
**What adoption changes:**
|
|
272
|
+
- A `<!-- claude-mem-lite:begin v1 -->…<!-- claude-mem-lite:end -->` block is
|
|
273
|
+
added to `MEMORY.md` under a `## 插件契约` header, containing one ≤150-char
|
|
274
|
+
line pointing at `mem_recall` / `mem_save` with their key arguments.
|
|
275
|
+
- A `plugin_claude_mem_lite.md` detail file is written (not auto-loaded; read
|
|
276
|
+
on demand when the MEMORY.md pointer surfaces in context).
|
|
277
|
+
- Post-adopt the conservative hook layer auto-trims: MCP server instructions
|
|
278
|
+
drop the `WHEN TO USE` section, SessionStart injection drops the `File Lessons`
|
|
279
|
+
/ `Key Context` sections. `#ID` references and the `Recent` table still fire
|
|
280
|
+
so `mem_get` remains reachable.
|
|
281
|
+
|
|
282
|
+
**Safety:**
|
|
283
|
+
- Hash-guarded: editing the sentinel body yourself blocks automatic rewrites
|
|
284
|
+
unless you pass `--force`.
|
|
285
|
+
- Budget-gated: refuses to insert when MEMORY.md is already >180 lines, so
|
|
286
|
+
Claude Code's 200-line MEMORY.md cap won't truncate the block.
|
|
287
|
+
- `install` only auto-adopts when run from the claude-mem-lite source repo
|
|
288
|
+
itself (detected via git remote match); other users must opt in explicitly.
|
|
289
|
+
- The fallback hook layer is never removed from source — conditional trim is
|
|
290
|
+
runtime-gated on sentinel presence, so projects without adoption get the
|
|
291
|
+
full verbose output.
|
|
292
|
+
|
|
293
|
+
See `docs/plans/2026-04-16-invited-memory-pattern.md` for the full design
|
|
294
|
+
(including the reusable template other plugins can follow).
|
|
295
|
+
|
|
248
296
|
## Database Schema
|
|
249
297
|
|
|
250
298
|
Five core tables with FTS5 virtual tables for search:
|
|
@@ -523,6 +571,8 @@ npm run benchmark:gate # CI gate: fails if metrics regress beyond 5% toleranc
|
|
|
523
571
|
| `CLAUDE_MEM_DIR` | Custom data directory. All databases, runtime files, and managed resources are stored here. | `~/.claude-mem-lite/` |
|
|
524
572
|
| `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
|
|
525
573
|
| `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
|
|
574
|
+
| `MEM_QUIET_HOOKS` | Low-noise hooks. `1` drops the `File Lessons` / `Key Context` sections from SessionStart injection, the lesson suffix from `[mem] Related memories`, and the `WHEN TO USE` / `Decision rules` blocks from MCP server instructions. IDs and the `Recent` table still surface so `mem_get(ids=[…])` remains reachable. Intended for users running the invited-memory adopt path or who otherwise want minimal auto-injection. | _(disabled)_ |
|
|
575
|
+
| `MEM_NO_ADOPT_HINT` | Silences the one-line "Invited-memory 未启用:`claude-mem-lite adopt`…" hint that SessionStart appends when the current project hasn't been adopted. The hint self-clears once `adopt` runs; set this env to suppress it without adopting. | _(disabled)_ |
|
|
526
576
|
|
|
527
577
|
## License
|
|
528
578
|
|
package/README.zh-CN.md
CHANGED
|
@@ -94,6 +94,10 @@
|
|
|
94
94
|
- **LLM 并发控制** -- 基于文件的信号量将后台 worker 限制为 2 个并发 LLM 调用,防止资源争用
|
|
95
95
|
- **stdin 溢出保护** -- Hook 输入在 256KB 处截断,对超大工具输出使用正则挽救关键信息
|
|
96
96
|
- **跨会话交接** -- 在 `/clear` 或 `/exit` 时捕获会话状态(请求、已完成工作、后续步骤、关键文件),下次会话检测到继续意图时自动注入上下文(支持显式关键词和 FTS5 术语重叠匹配)
|
|
97
|
+
- **插件缓存 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 起)。
|
|
98
|
+
- **Git-SHA 延续锚点**(v2.31.0)-- handoff 记录包含 `git_sha_at_handoff` 字段,任何匹配当前 `HEAD` 的 handoff 都视为延续会话,不受 TTL 限制。代码状态比时钟时间更能反映上下文延续。
|
|
99
|
+
- **启动面板**(v2.31.0)-- SessionStart hook 将 `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + 最近 /exit 交接 + 最近事件数聚合为一个结构化块,通过 `hookSpecificOutput.additionalContext` 注入。
|
|
100
|
+
- **活动命名空间**(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`;斜杠命令:`/lesson`、`/bug`。`hook-llm` 通过 `persistHaikuSummary` 路由非 memdir 摘要,observations → events 升级路径是事务原子的。
|
|
97
101
|
|
|
98
102
|
## 平台支持
|
|
99
103
|
|
|
@@ -222,6 +226,8 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
222
226
|
/mem timeline <query> # 围绕匹配结果浏览时间线
|
|
223
227
|
/mem browse # 分层记忆仪表盘
|
|
224
228
|
/mem <query> # search 的简写
|
|
229
|
+
/lesson <text> # 保存非显而易见的经验到 events 表(v2.31.0)
|
|
230
|
+
/bug <text> # 记录已知 bug + 复现步骤到 events 表(v2.31.0)
|
|
225
231
|
```
|
|
226
232
|
|
|
227
233
|
### 高效搜索工作流
|
|
@@ -232,6 +238,44 @@ rm -rf ~/claude-mem-lite/ # v0.5 前的非隐藏目录(如未自动迁移)
|
|
|
232
238
|
3. mem_get(ids=[12345, 12346]) -> 完整详情
|
|
233
239
|
```
|
|
234
240
|
|
|
241
|
+
### Invited Memory(邀请式记忆,v2.32+)
|
|
242
|
+
|
|
243
|
+
Opt-in 机制——向项目 memdir(`~/.claude/projects/<encoded>/memory/MEMORY.md`)
|
|
244
|
+
注入单行 sentinel 包围的插件契约,Claude Code 会把它作为 **user-memory** 加载
|
|
245
|
+
到系统提示——比 MCP server instructions(被框定为 tool metadata)具有更高的
|
|
246
|
+
instruction-following 权威。
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
claude-mem-lite adopt # 当前项目注入
|
|
250
|
+
claude-mem-lite adopt --all # 扫描 ~/.claude/projects/* 全部注入
|
|
251
|
+
claude-mem-lite adopt --status # 列出所有已 adopt 的项目 + 版本号
|
|
252
|
+
claude-mem-lite adopt --dry-run # 只打印不写入
|
|
253
|
+
claude-mem-lite unadopt # 精确移除
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Slash 命令 `/adopt` 和 `/unadopt` 是上述 CLI 的包装。
|
|
257
|
+
|
|
258
|
+
**Adopt 后会发生什么:**
|
|
259
|
+
- `MEMORY.md` 新增一段 `<!-- claude-mem-lite:begin v1 -->…<!-- claude-mem-lite:end -->`
|
|
260
|
+
包裹的 `## 插件契约`,含一行 ≤150 字符、指向 `mem_recall` / `mem_save` 关键参数
|
|
261
|
+
的动作锚条目。
|
|
262
|
+
- 生成 `plugin_claude_mem_lite.md` 详情文件(按需读取,不自动加载)。
|
|
263
|
+
- 保守 hook 层自动瘦身:MCP server instructions 去掉 `WHEN TO USE` 段,
|
|
264
|
+
SessionStart 注入去掉 `File Lessons` / `Key Context`。`#ID` 引用与 `Recent`
|
|
265
|
+
表保留,`mem_get` 仍可随时展开。
|
|
266
|
+
|
|
267
|
+
**安全性:**
|
|
268
|
+
- Hash 守护:你手动改了 sentinel 段 → 下一次 adopt 报 `UserEditedError`,
|
|
269
|
+
除非显式 `--force`。
|
|
270
|
+
- 预算门:MEMORY.md 已 >180 行时拒绝新增(避开 Claude Code 200 行截断)。
|
|
271
|
+
- `install` 只在从 claude-mem-lite 源码仓库运行时 auto-adopt(靠 git remote 判别);
|
|
272
|
+
其它用户需显式调用。
|
|
273
|
+
- 保守 hook 层源码永不删——条件瘦身仅基于 sentinel 存在性做 runtime 判断,
|
|
274
|
+
未 adopt 的项目仍看完整 verbose 输出。
|
|
275
|
+
|
|
276
|
+
完整设计见 `docs/plans/2026-04-16-invited-memory-pattern.md`(含其它插件
|
|
277
|
+
可复用的模板)。
|
|
278
|
+
|
|
235
279
|
## 数据库结构
|
|
236
280
|
|
|
237
281
|
五张核心表 + FTS5 虚拟表用于搜索:
|
|
@@ -500,6 +544,8 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
|
|
|
500
544
|
| `CLAUDE_MEM_DIR` | 自定义数据目录。所有数据库、运行时文件和托管资源均存储在此。 | `~/.claude-mem-lite/` |
|
|
501
545
|
| `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
|
|
502
546
|
| `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
|
|
547
|
+
| `MEM_QUIET_HOOKS` | 低噪声 hook。设为 `1` 时,SessionStart 注入去掉 `File Lessons` / `Key Context` 两节,`[mem] Related memories` 去掉 lesson 后缀,MCP server instructions 去掉 `WHEN TO USE` / `Decision rules` 两段。ID 与 `Recent` 表仍保留,`mem_get(ids=[…])` 可继续展开细节。适用于启用了 invited-memory adopt 流程或偏好最小化自动注入的用户。 | _(禁用)_ |
|
|
548
|
+
| `MEM_NO_ADOPT_HINT` | 静音当前项目未 adopt 时 SessionStart 追加的那一行 "Invited-memory 未启用…" 提示。一旦运行 `adopt` 该提示自动消失;此 env 让偏好保守层的用户在不 adopt 的情况下也能静音。 | _(禁用)_ |
|
|
503
549
|
|
|
504
550
|
## 许可证
|
|
505
551
|
|
package/adopt-cli.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Phase C (Invited-Memory plan, T11): CLI handlers for
|
|
2
|
+
// claude-mem-lite adopt [--all] [--force] [--dry-run] [--status]
|
|
3
|
+
// claude-mem-lite unadopt [--all]
|
|
4
|
+
//
|
|
5
|
+
// adopt = write sentinel section into MEMORY.md + drop plugin_claude_mem_lite.md
|
|
6
|
+
// unadopt = precise sentinel removal + doc cleanup
|
|
7
|
+
// --all = scan every project under ~/.claude/projects/*/memory/
|
|
8
|
+
// --force = override UserEditedError
|
|
9
|
+
// --dry-run = print intent without writing
|
|
10
|
+
// --status = list all adopted projects + versions
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import {
|
|
16
|
+
memdirPath, writePluginSection, removePluginSection,
|
|
17
|
+
writePluginDoc, removePluginDoc,
|
|
18
|
+
isAdopted, readMemoryIndex,
|
|
19
|
+
UserEditedError, BudgetExceededError,
|
|
20
|
+
} from './memdir.mjs';
|
|
21
|
+
import {
|
|
22
|
+
PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, getIndexLine, getDetailDoc,
|
|
23
|
+
} from './adopt-content.mjs';
|
|
24
|
+
|
|
25
|
+
function log(msg) { console.log(msg); }
|
|
26
|
+
|
|
27
|
+
function detectCwd() {
|
|
28
|
+
return process.env.CLAUDE_PROJECT_DIR || process.env.PWD || process.cwd();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function projectsRoot() {
|
|
32
|
+
return join(homedir(), '.claude', 'projects');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function listAllMemdirs() {
|
|
36
|
+
const base = projectsRoot();
|
|
37
|
+
if (!existsSync(base)) return [];
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const name of readdirSync(base)) {
|
|
40
|
+
const memdir = join(base, name, 'memory');
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(memdir) && statSync(memdir).isDirectory()) {
|
|
43
|
+
out.push({ projectSlug: name, memdir });
|
|
44
|
+
}
|
|
45
|
+
} catch { /* ignore entries we can't stat */ }
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasFlag(args, flag) { return Array.isArray(args) && args.includes(flag); }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* cmdAdopt — write sentinel section + plugin doc to memdir.
|
|
54
|
+
* Exit code 1 on any hard failure; skipped (--all + UserEditedError) doesn't
|
|
55
|
+
* fail the batch.
|
|
56
|
+
*/
|
|
57
|
+
export function cmdAdopt(args = []) {
|
|
58
|
+
if (hasFlag(args, '--status')) return statusAll();
|
|
59
|
+
|
|
60
|
+
const all = hasFlag(args, '--all');
|
|
61
|
+
const force = hasFlag(args, '--force');
|
|
62
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
63
|
+
|
|
64
|
+
const targets = all
|
|
65
|
+
? listAllMemdirs().map((m) => m.memdir)
|
|
66
|
+
: [memdirPath(detectCwd())];
|
|
67
|
+
|
|
68
|
+
if (targets.length === 0) {
|
|
69
|
+
log('[adopt] no memdirs to adopt (use without --all for current project)');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let created = 0, updated = 0, unchanged = 0, skipped = 0, failed = 0;
|
|
74
|
+
for (const memdir of targets) {
|
|
75
|
+
const r = adoptOne(memdir, { force, dryRun, all });
|
|
76
|
+
if (r.action === 'created') created++;
|
|
77
|
+
else if (r.action === 'updated') updated++;
|
|
78
|
+
else if (r.action === 'unchanged') unchanged++;
|
|
79
|
+
else if (r.action === 'skipped') skipped++;
|
|
80
|
+
else if (r.action === 'dry-run') unchanged++;
|
|
81
|
+
else failed++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log('');
|
|
85
|
+
log(`[adopt] ${targets.length} target(s): ${created} created, ${updated} updated, ${unchanged} unchanged, ${skipped} skipped, ${failed} failed`);
|
|
86
|
+
if (failed > 0) process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function adoptOne(memdir, { force, dryRun, all }) {
|
|
90
|
+
const contentLine = getIndexLine();
|
|
91
|
+
const version = CURRENT_SENTINEL_VERSION;
|
|
92
|
+
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
log(`[adopt --dry-run] ${memdir}`);
|
|
95
|
+
log(` MEMORY.md line: ${contentLine}`);
|
|
96
|
+
log(` detail file: plugin_claude_mem_lite.md (${getDetailDoc().length} chars)`);
|
|
97
|
+
return { action: 'dry-run' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const r = writePluginSection(memdir, { slug: PLUGIN_SLUG, version, contentLine, force });
|
|
102
|
+
writePluginDoc(memdir, PLUGIN_SLUG, getDetailDoc());
|
|
103
|
+
log(`[adopt] ${memdir} → ${r.action}`);
|
|
104
|
+
return r;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (e instanceof UserEditedError && all) {
|
|
107
|
+
log(`[adopt] ${memdir} → skipped (user-edited; pass --force to override)`);
|
|
108
|
+
return { action: 'skipped' };
|
|
109
|
+
}
|
|
110
|
+
if (e instanceof UserEditedError) {
|
|
111
|
+
log(`[adopt] ${memdir} → refused: ${e.message}`);
|
|
112
|
+
log('[adopt] pass --force to overwrite, or edit/uninstall manually.');
|
|
113
|
+
return { action: 'failed' };
|
|
114
|
+
}
|
|
115
|
+
if (e instanceof BudgetExceededError) {
|
|
116
|
+
log(`[adopt] ${memdir} → failed: ${e.message}`);
|
|
117
|
+
return { action: 'failed' };
|
|
118
|
+
}
|
|
119
|
+
log(`[adopt] ${memdir} → error: ${e.message}`);
|
|
120
|
+
return { action: 'failed' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function statusAll() {
|
|
125
|
+
const dirs = listAllMemdirs();
|
|
126
|
+
log('[adopt --status] scanning ~/.claude/projects/*/memory/');
|
|
127
|
+
if (dirs.length === 0) { log(' (no memdirs found)'); return; }
|
|
128
|
+
let adopted = 0;
|
|
129
|
+
for (const { projectSlug, memdir } of dirs) {
|
|
130
|
+
if (isAdopted(memdir, PLUGIN_SLUG)) {
|
|
131
|
+
const idx = readMemoryIndex(memdir, PLUGIN_SLUG);
|
|
132
|
+
log(` ✓ ${projectSlug} (${idx.version})`);
|
|
133
|
+
adopted++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
log('');
|
|
137
|
+
log(`[adopt --status] ${adopted}/${dirs.length} adopted`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* cmdUnadopt — precise removal of sentinel section + plugin doc.
|
|
142
|
+
* Exit code stays 0: unadopt is idempotent; "absent" isn't an error.
|
|
143
|
+
*/
|
|
144
|
+
export function cmdUnadopt(args = []) {
|
|
145
|
+
const all = hasFlag(args, '--all');
|
|
146
|
+
const targets = all
|
|
147
|
+
? listAllMemdirs().map((m) => m.memdir)
|
|
148
|
+
: [memdirPath(detectCwd())];
|
|
149
|
+
|
|
150
|
+
if (targets.length === 0) {
|
|
151
|
+
log('[unadopt] no memdirs found');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let removed = 0, absent = 0;
|
|
156
|
+
for (const memdir of targets) {
|
|
157
|
+
const r = removePluginSection(memdir, PLUGIN_SLUG);
|
|
158
|
+
removePluginDoc(memdir, PLUGIN_SLUG);
|
|
159
|
+
if (r.action === 'removed') removed++;
|
|
160
|
+
else absent++;
|
|
161
|
+
log(`[unadopt] ${memdir} → ${r.action}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
log('');
|
|
165
|
+
log(`[unadopt] ${targets.length} target(s): ${removed} removed, ${absent} absent`);
|
|
166
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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.
|
|
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.
|
|
9
|
+
|
|
10
|
+
export const PLUGIN_SLUG = 'claude-mem-lite';
|
|
11
|
+
export const CURRENT_SENTINEL_VERSION = 'v1';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One-line entry injected into MEMORY.md's sentinel section. Must stay ≤150
|
|
15
|
+
* chars so it clears Claude Code's 200-line MEMORY.md cap with headroom.
|
|
16
|
+
*/
|
|
17
|
+
export function getIndexLine() {
|
|
18
|
+
return '- [claude-mem-lite](plugin_claude_mem_lite.md) — Edit 前 `mem_recall(file=…)`;bugfix 后 `mem_save(type="bugfix", lesson_learned, importance=2)`';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Full detail doc rendered into `memdir/plugin_claude_mem_lite.md`. Not
|
|
23
|
+
* auto-loaded by Claude Code — the MEMORY.md index line points to it and
|
|
24
|
+
* Claude reads it on demand when the injected hint suggests relevance.
|
|
25
|
+
*/
|
|
26
|
+
export function getDetailDoc() {
|
|
27
|
+
return `# claude-mem-lite 插件契约
|
|
28
|
+
|
|
29
|
+
> 由 \`claude-mem-lite adopt\` 生成;卸载用 \`claude-mem-lite unadopt\`。
|
|
30
|
+
> 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
|
|
31
|
+
|
|
32
|
+
## 何时调用 MCP 工具
|
|
33
|
+
|
|
34
|
+
| 时机 | 工具 | 关键参数 |
|
|
35
|
+
|------|------|----------|
|
|
36
|
+
| Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\` —— 过往 bugfix 与教训 |
|
|
37
|
+
| Test failure / error | \`mem_search\` | \`query="<错误关键词>", obs_type="bugfix"\` |
|
|
38
|
+
| Refactor 前 | \`mem_search\` | \`query="<模块>", obs_type="refactor"\` |
|
|
39
|
+
| 新功能起手 | \`mem_search\` | \`query="<功能区域>"\` —— 找 prior art |
|
|
40
|
+
| Bugfix 解决后 | \`mem_save\` | \`type="bugfix", lesson_learned="<根因+修法>", importance=2\` |
|
|
41
|
+
| 架构决策后 | \`mem_save\` | \`type="decision", lesson_learned="<取舍理由>", importance=2\` |
|
|
42
|
+
| 上下文提到 #NN | \`mem_get\` | \`ids=[NN]\` |
|
|
43
|
+
|
|
44
|
+
## Decision rules(替代多步 search)
|
|
45
|
+
|
|
46
|
+
- "最近做了啥" → \`mem_recent\`
|
|
47
|
+
- "<文件> 有哪些记忆" → \`mem_recall\`
|
|
48
|
+
- "#NN 前后发生了啥" → \`mem_timeline\`
|
|
49
|
+
- "清理过期记忆" → \`mem_maintain\`
|
|
50
|
+
- "FTS 索引健康吗" → \`mem_fts_check\`
|
|
51
|
+
- "按 tier 浏览" → \`mem_browse\`
|
|
52
|
+
- "备份导出" → \`mem_export\`
|
|
53
|
+
|
|
54
|
+
## CLI 速查
|
|
55
|
+
|
|
56
|
+
| 命令 | 用途 |
|
|
57
|
+
|------|------|
|
|
58
|
+
| \`claude-mem-lite search "query"\` | FTS5 全文搜索 |
|
|
59
|
+
| \`claude-mem-lite search "err" --type bugfix\` | 按类型过滤 |
|
|
60
|
+
| \`claude-mem-lite recall "file.mjs"\` | 文件相关记忆 |
|
|
61
|
+
| \`claude-mem-lite recent 5\` | 最近 5 条 |
|
|
62
|
+
| \`claude-mem-lite get 42,43\` | 按 ID 展开 |
|
|
63
|
+
| \`claude-mem-lite timeline --anchor 42\` | 时间线上下文 |
|
|
64
|
+
|
|
65
|
+
## 质量门槛
|
|
66
|
+
|
|
67
|
+
- \`mem_save\` 的 \`lesson_learned\` 不要写 \`none\`——写不出教训就保持 NULL
|
|
68
|
+
- \`decision\` 命中率 72.7% vs \`change\` 16.5%——一条好 decision ≈ 20 条 change
|
|
69
|
+
- 一般搜索跳过 \`obs_type\` 让系统自动路由;特定意图再过滤
|
|
70
|
+
|
|
71
|
+
## 卸载
|
|
72
|
+
|
|
73
|
+
\`claude-mem-lite unadopt\` 精确移除 sentinel 段 + 本文件;其它 MEMORY.md 内容不动。
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /adopt
|
|
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.
|
|
13
|
+
|
|
14
|
+
## What it writes
|
|
15
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Flags
|
|
25
|
+
|
|
26
|
+
- `--force` — overwrite a sentinel block that was manually edited
|
|
27
|
+
- `--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
|
|
30
|
+
|
|
31
|
+
## Removal
|
|
32
|
+
|
|
33
|
+
`/unadopt` precisely removes the sentinel block + plugin doc + state file.
|
|
34
|
+
User content outside the sentinel is preserved.
|
|
35
|
+
|
|
36
|
+
## Conservative layer still active
|
|
37
|
+
|
|
38
|
+
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
|
+
!claude-mem-lite adopt $ARGUMENTS
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /unadopt
|
|
7
|
+
|
|
8
|
+
Remove the claude-mem-lite invited-memory sentinel from the current project's
|
|
9
|
+
Claude Code memdir. Opposite of `/adopt`.
|
|
10
|
+
|
|
11
|
+
## What it removes
|
|
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.
|
|
17
|
+
|
|
18
|
+
Everything else in `MEMORY.md` is preserved byte-for-byte.
|
|
19
|
+
|
|
20
|
+
## Flags
|
|
21
|
+
|
|
22
|
+
- `--all` — unadopt every memdir under `~/.claude/projects/*/memory/`
|
|
23
|
+
|
|
24
|
+
## Aftermath
|
|
25
|
+
|
|
26
|
+
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.
|
|
29
|
+
|
|
30
|
+
!claude-mem-lite unadopt $ARGUMENTS
|
package/hook-context.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
debugLog, debugCatch,
|
|
9
9
|
DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause,
|
|
10
10
|
} from './utils.mjs';
|
|
11
|
-
import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS } from './hook-shared.mjs';
|
|
11
|
+
import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, effectiveQuiet } from './hook-shared.mjs';
|
|
12
12
|
import { extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -315,18 +315,25 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
315
315
|
keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// Phase A (QUIET_HOOKS) + Phase D (adopted sentinel): drop descriptive
|
|
319
|
+
// File Lessons / Key Context sections when the user has opted into low-noise
|
|
320
|
+
// hooks OR adopted invited-memory (MEMORY.md sentinel carries the triggers
|
|
321
|
+
// at higher system-prompt authority). The Recent table still fires so #IDs
|
|
322
|
+
// remain reachable via mem_get.
|
|
323
|
+
const quiet = effectiveQuiet();
|
|
324
|
+
if (fileLessons.length > 0 && !quiet) {
|
|
319
325
|
summaryLines.push('### File Lessons');
|
|
320
326
|
summaryLines.push(...fileLessons.slice(0, 5));
|
|
321
327
|
summaryLines.push('');
|
|
322
328
|
}
|
|
323
|
-
if (keyContext.length > 0) {
|
|
329
|
+
if (keyContext.length > 0 && !quiet) {
|
|
324
330
|
summaryLines.push('### Key Context');
|
|
325
331
|
summaryLines.push(...keyContext.slice(0, 5));
|
|
326
332
|
summaryLines.push('');
|
|
327
333
|
}
|
|
328
|
-
} else if (!latestSummary) {
|
|
329
|
-
// Fallback: no summary AND no key observations — show recent activity
|
|
334
|
+
} else if (!latestSummary && !effectiveQuiet()) {
|
|
335
|
+
// Fallback: no summary AND no key observations — show recent activity.
|
|
336
|
+
// Skipped under QUIET_HOOKS since the Recent table already carries titles.
|
|
330
337
|
const recentObs = (observations.length >= 3 ? observations : fallbackObs).slice(0, 3);
|
|
331
338
|
if (recentObs.length > 0) {
|
|
332
339
|
summaryLines.push('### Recent Activity');
|
package/hook-shared.mjs
CHANGED
|
@@ -8,6 +8,10 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from '
|
|
|
8
8
|
import { inferProject, debugCatch } from './utils.mjs';
|
|
9
9
|
import { ensureDb, DB_DIR } from './schema.mjs';
|
|
10
10
|
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared } from './haiku-client.mjs';
|
|
11
|
+
// Phase D: invited-memory sentinel detection. memdir.mjs only pulls in fs/path/os/crypto;
|
|
12
|
+
// adopt-content.mjs is pure strings. No circular deps — memdir doesn't import hook-shared.
|
|
13
|
+
import { memdirPath as _memdirPath, isAdopted as _isAdopted } from './memdir.mjs';
|
|
14
|
+
import { PLUGIN_SLUG as _PLUGIN_SLUG } from './adopt-content.mjs';
|
|
11
15
|
|
|
12
16
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
13
17
|
|
|
@@ -24,6 +28,33 @@ export const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 min (title dedup)
|
|
|
24
28
|
export const RELATED_OBS_WINDOW_MS = 7 * 86400000; // 7 days
|
|
25
29
|
export const FALLBACK_OBS_WINDOW_MS = RELATED_OBS_WINDOW_MS; // same window
|
|
26
30
|
|
|
31
|
+
// Phase A (v2.31.3+): MEM_QUIET_HOOKS=1 drops descriptive hook/MCP-instruction
|
|
32
|
+
// bodies (File Lessons / Key Context headers, MCP WHEN-TO-USE & decision rules,
|
|
33
|
+
// related-memory lesson suffix). Intended for users who adopted invited-memory
|
|
34
|
+
// (MEMORY.md sentinel) or who otherwise want minimal hook noise. Function form
|
|
35
|
+
// (not const) so modules importing at load time still respect later env sets
|
|
36
|
+
// in-process, and tests can toggle per-call. See docs/plans/2026-04-16-invited-memory-pattern.md.
|
|
37
|
+
export function isQuietHooks() {
|
|
38
|
+
return process.env.MEM_QUIET_HOOKS === '1';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Phase D (v2.32.1+): if the current project has adopted our invited-memory
|
|
42
|
+
// sentinel, MEMORY.md already carries the triggers at system-prompt authority
|
|
43
|
+
// — so hook + MCP-instruction output can also go quiet. isQuietHooks (env)
|
|
44
|
+
// remains an independent, stronger override.
|
|
45
|
+
export function isAdoptedHere(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const resolved = cwd || process.env.CLAUDE_PROJECT_DIR || process.env.PWD || process.cwd();
|
|
48
|
+
return _isAdopted(_memdirPath(resolved), _PLUGIN_SLUG);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function effectiveQuiet(cwd) {
|
|
55
|
+
return isQuietHooks() || isAdoptedHere(cwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
27
58
|
// Handoff system constants
|
|
28
59
|
export const HANDOFF_EXPIRY_CLEAR = 6 * 3600000; // 6 hours (covers lunch/meeting breaks)
|
|
29
60
|
export const HANDOFF_EXPIRY_EXIT = 7 * 24 * 60 * 60 * 1000; // 7 days
|
package/install.mjs
CHANGED
|
@@ -224,6 +224,10 @@ async function install() {
|
|
|
224
224
|
'lib/plan-reader.mjs',
|
|
225
225
|
'lib/git-state.mjs',
|
|
226
226
|
'lib/startup-dashboard.mjs',
|
|
227
|
+
// v2.32 (invited-memory): memdir primitives + adopt/unadopt CLI.
|
|
228
|
+
'memdir.mjs',
|
|
229
|
+
'adopt-content.mjs',
|
|
230
|
+
'adopt-cli.mjs',
|
|
227
231
|
];
|
|
228
232
|
|
|
229
233
|
if (IS_DEV) {
|
|
@@ -807,6 +811,25 @@ async function install() {
|
|
|
807
811
|
log('No existing database — will be created on first use');
|
|
808
812
|
}
|
|
809
813
|
|
|
814
|
+
// 7b. Dogfood auto-adopt (invited-memory, Phase C T13).
|
|
815
|
+
// Only fires when install.mjs is running from the claude-mem-lite source repo
|
|
816
|
+
// itself (detected via git remote match). In npm/npx flows PROJECT_DIR is a
|
|
817
|
+
// cache dir with no git metadata, so this is a no-op for end users.
|
|
818
|
+
// --no-adopt override respected.
|
|
819
|
+
if (!flags.has('--no-adopt')) {
|
|
820
|
+
try {
|
|
821
|
+
const remote = execFileSync('git', ['-C', PROJECT_DIR, 'config', '--get', 'remote.origin.url'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
822
|
+
const isDogfood = /github\.com[:/]sdsrss\/claude-mem-lite(\.git)?$/i.test(remote);
|
|
823
|
+
if (isDogfood) {
|
|
824
|
+
const { cmdAdopt } = await import('./adopt-cli.mjs');
|
|
825
|
+
cmdAdopt([]);
|
|
826
|
+
ok('Invited-memory: auto-adopt for claude-mem-lite dogfood repo');
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
// Not a git repo, or git missing — silent skip (this is the normal npm path).
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
810
833
|
// 8. Disable old claude-mem plugin
|
|
811
834
|
if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) {
|
|
812
835
|
settings.enabledPlugins['claude-mem@thedotmack'] = false;
|
|
@@ -851,6 +874,10 @@ async function uninstall() {
|
|
|
851
874
|
const settings = readSettings();
|
|
852
875
|
cleanupMemHooksFromSettings(settings);
|
|
853
876
|
|
|
877
|
+
// 2b. Uninstall does NOT auto-unadopt — an adopted project may be in active use
|
|
878
|
+
// by the user in other Claude Code sessions. Tell them the command instead.
|
|
879
|
+
log('Invited-memory: adopt state preserved. Run `claude-mem-lite unadopt --all` to remove sentinel sections.');
|
|
880
|
+
|
|
854
881
|
// 3. Clean plugin registry entries conservatively (avoid deleting other plugins
|
|
855
882
|
// from the same marketplace publisher)
|
|
856
883
|
const pluginsDir = join(homedir(), '.claude', 'plugins');
|
package/mem-cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
|
14
14
|
import { searchResources } from './registry-retriever.mjs';
|
|
15
15
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
16
16
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
17
|
+
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
17
18
|
import { basename } from 'path';
|
|
18
19
|
import { readFileSync } from 'fs';
|
|
19
20
|
|
|
@@ -2058,6 +2059,17 @@ Commands:
|
|
|
2058
2059
|
--files (plural, comma-split) preferred; --file (singular) kept for back-compat.
|
|
2059
2060
|
Use /lesson or /bug slash commands for faster capture (T8).
|
|
2060
2061
|
|
|
2062
|
+
adopt Inject claude-mem-lite sentinel line into this project's
|
|
2063
|
+
~/.claude/projects/<encoded>/memory/MEMORY.md so Claude Code
|
|
2064
|
+
auto-loads it as user-memory (higher instruction authority).
|
|
2065
|
+
--all Adopt every project under ~/.claude/projects/*/memory/
|
|
2066
|
+
--force Overwrite a sentinel block that was manually edited
|
|
2067
|
+
--dry-run Print intended writes without touching disk
|
|
2068
|
+
--status List adopted projects + version
|
|
2069
|
+
|
|
2070
|
+
unadopt Precise removal of the sentinel block + plugin_claude_mem_lite.md.
|
|
2071
|
+
--all Unadopt every project
|
|
2072
|
+
|
|
2061
2073
|
DB: ${DB_PATH}`);
|
|
2062
2074
|
}
|
|
2063
2075
|
|
|
@@ -2336,6 +2348,11 @@ export async function run(argv) {
|
|
|
2336
2348
|
return;
|
|
2337
2349
|
}
|
|
2338
2350
|
|
|
2351
|
+
// adopt / unadopt do pure filesystem work on ~/.claude/projects/<encoded>/memory/ —
|
|
2352
|
+
// no DB needed. Route them before ensureDb() so an unbootable DB doesn't block.
|
|
2353
|
+
if (cmd === 'adopt') { cmdAdopt(cmdArgs); return; }
|
|
2354
|
+
if (cmd === 'unadopt') { cmdUnadopt(cmdArgs); return; }
|
|
2355
|
+
|
|
2339
2356
|
let db;
|
|
2340
2357
|
try {
|
|
2341
2358
|
db = ensureDb();
|
package/memdir.mjs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Phase B (Invited-Memory plan): memdir.mjs — primitives for the per-project
|
|
2
|
+
// Claude Code memdir at ~/.claude/projects/<encoded>/memory/.
|
|
3
|
+
//
|
|
4
|
+
// The public API lets a plugin install a single sentinel-wrapped line into
|
|
5
|
+
// MEMORY.md (auto-loaded by Claude Code into the system prompt) plus an
|
|
6
|
+
// on-demand `plugin_<slug>.md` detail file. Writes are idempotent, hash-guarded
|
|
7
|
+
// against manual user edits, and capped by a 180-line budget so the injection
|
|
8
|
+
// block never gets truncated by Claude Code's 200-line MEMORY.md cap.
|
|
9
|
+
//
|
|
10
|
+
// See docs/plans/2026-04-16-invited-memory-pattern.md for rationale.
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, mkdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
|
|
17
|
+
const MEMORY_LINE_BUDGET = 180;
|
|
18
|
+
const SECTION_HEADER = '## 插件契约';
|
|
19
|
+
|
|
20
|
+
export class UserEditedError extends Error {
|
|
21
|
+
constructor(message) { super(message); this.name = 'UserEditedError'; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BudgetExceededError extends Error {
|
|
25
|
+
constructor(message) { super(message); this.name = 'BudgetExceededError'; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mirror Claude Code's project-path encoding: every non-alphanumeric char
|
|
32
|
+
* is replaced with "-". Lossy by design — the encoded path is a directory
|
|
33
|
+
* slug, not a reversible encoding. Ground truth fixture:
|
|
34
|
+
* /mnt/data_ssd/dev/projects/mem → -mnt-data-ssd-dev-projects-mem
|
|
35
|
+
* Memory ref: #7687 ("Claude Code mangles EVERY non-alphanumeric char").
|
|
36
|
+
*/
|
|
37
|
+
export function encodeProjectPath(absPath) {
|
|
38
|
+
return String(absPath).replace(/[^a-zA-Z0-9]/g, '-');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Absolute path to the project's memdir. Caller runs mkdir-p as needed.
|
|
43
|
+
*/
|
|
44
|
+
export function memdirPath(projectCwd) {
|
|
45
|
+
return join(homedir(), '.claude', 'projects', encodeProjectPath(projectCwd), 'memory');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function memoryFile(memdir) { return join(memdir, 'MEMORY.md'); }
|
|
49
|
+
|
|
50
|
+
function slugSnake(slug) {
|
|
51
|
+
return String(slug).replace(/[^a-zA-Z0-9]/g, '_');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function docFile(memdir, slug) {
|
|
55
|
+
return join(memdir, `plugin_${slugSnake(slug)}.md`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stateFile(memdir, slug) {
|
|
59
|
+
return join(memdir, `.plugin_${slugSnake(slug)}_state.json`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Sentinel rendering & parsing ────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function sentinelRegex(slug) {
|
|
65
|
+
// Escape regex metacharacters in the slug. In practice plugin slugs are
|
|
66
|
+
// [a-z0-9-], but guard against '.', '+' etc. from arbitrary other plugins.
|
|
67
|
+
const esc = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
return new RegExp(
|
|
69
|
+
`<!-- ${esc}:begin (v\\d+) -->\\s*\\n([\\s\\S]*?)<!-- ${esc}:end -->`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderSentinel(slug, version, contentLine) {
|
|
74
|
+
return [
|
|
75
|
+
`<!-- ${slug}:begin ${version} -->`,
|
|
76
|
+
SECTION_HEADER,
|
|
77
|
+
contentLine,
|
|
78
|
+
`<!-- ${slug}:end -->`,
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function canonicalBody(contentLine) {
|
|
83
|
+
// Must match what sentinelRegex captures in match[2]: everything between
|
|
84
|
+
// "begin X -->\n" and "<!-- X:end -->". That's SECTION_HEADER + '\n' +
|
|
85
|
+
// contentLine + '\n'.
|
|
86
|
+
return `${SECTION_HEADER}\n${contentLine}\n`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256(s) { return createHash('sha256').update(s).digest('hex'); }
|
|
90
|
+
|
|
91
|
+
function atomicWrite(path, content) {
|
|
92
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
93
|
+
writeFileSync(tmp, content);
|
|
94
|
+
renameSync(tmp, path);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readState(memdir, slug) {
|
|
98
|
+
const p = stateFile(memdir, slug);
|
|
99
|
+
if (!existsSync(p)) return null;
|
|
100
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeState(memdir, slug, state) {
|
|
104
|
+
atomicWrite(stateFile(memdir, slug), JSON.stringify(state, null, 2) + '\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function clearState(memdir, slug) {
|
|
108
|
+
const p = stateFile(memdir, slug);
|
|
109
|
+
if (existsSync(p)) try { unlinkSync(p); } catch { /* best-effort */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse MEMORY.md for the plugin's sentinel block.
|
|
116
|
+
* @returns {{ exists: boolean, raw: string, lineCount: number,
|
|
117
|
+
* section: string|null, body: string|null, version: string|null }}
|
|
118
|
+
*/
|
|
119
|
+
export function readMemoryIndex(memdir, slug) {
|
|
120
|
+
const path = memoryFile(memdir);
|
|
121
|
+
if (!existsSync(path)) {
|
|
122
|
+
return { exists: false, raw: '', lineCount: 0, section: null, body: null, version: null };
|
|
123
|
+
}
|
|
124
|
+
const raw = readFileSync(path, 'utf8');
|
|
125
|
+
const m = raw.match(sentinelRegex(slug));
|
|
126
|
+
const lineCount = raw.length === 0 ? 0 : raw.split('\n').length;
|
|
127
|
+
if (!m) return { exists: true, raw, lineCount, section: null, body: null, version: null };
|
|
128
|
+
return { exists: true, raw, lineCount, section: m[0], body: m[2], version: m[1] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Insert-or-update the plugin's sentinel block in MEMORY.md, writing a
|
|
133
|
+
* state sidecar alongside it.
|
|
134
|
+
*
|
|
135
|
+
* Idempotent: rewriting identical inputs is a no-op (returns action='unchanged').
|
|
136
|
+
*
|
|
137
|
+
* @param {string} memdir Absolute path to the memory directory.
|
|
138
|
+
* @param {object} opts
|
|
139
|
+
* @param {string} opts.slug Plugin slug (e.g. 'claude-mem-lite').
|
|
140
|
+
* @param {string} opts.version Contract version, e.g. 'v1'.
|
|
141
|
+
* @param {string} opts.contentLine Single-line index entry (≤150 chars).
|
|
142
|
+
* @param {boolean} [opts.force=false] Override UserEditedError.
|
|
143
|
+
* @returns {{action: 'created'|'updated'|'unchanged'}}
|
|
144
|
+
*
|
|
145
|
+
* @throws {BudgetExceededError} when MEMORY.md has > 180 lines AND we'd be
|
|
146
|
+
* adding a new section (updates to an existing sentinel are always allowed).
|
|
147
|
+
* @throws {UserEditedError} when the sentinel exists but its body hash doesn't
|
|
148
|
+
* match the last hash we wrote (detected via the state sidecar). Also thrown
|
|
149
|
+
* when a sentinel exists without any state file — treated as foreign content.
|
|
150
|
+
*/
|
|
151
|
+
export function writePluginSection(memdir, { slug, version, contentLine, force = false }) {
|
|
152
|
+
if (!existsSync(memdir)) mkdirSync(memdir, { recursive: true });
|
|
153
|
+
const path = memoryFile(memdir);
|
|
154
|
+
const raw = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
155
|
+
const match = raw.match(sentinelRegex(slug));
|
|
156
|
+
|
|
157
|
+
const freshSection = renderSentinel(slug, version, contentLine);
|
|
158
|
+
const freshHash = sha256(canonicalBody(contentLine));
|
|
159
|
+
|
|
160
|
+
if (!match) {
|
|
161
|
+
// Insert: enforce the 180-line budget so we never get truncated at 200.
|
|
162
|
+
const existingLines = raw.length === 0 ? 0 : raw.split('\n').length;
|
|
163
|
+
if (existingLines > MEMORY_LINE_BUDGET) {
|
|
164
|
+
throw new BudgetExceededError(
|
|
165
|
+
`MEMORY.md has ${existingLines} lines (> ${MEMORY_LINE_BUDGET}); refuse to add new sentinel section for ${slug}.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
// Assemble with one blank line before our section for readability.
|
|
169
|
+
let next;
|
|
170
|
+
if (raw.length === 0) {
|
|
171
|
+
next = freshSection + '\n';
|
|
172
|
+
} else if (raw.endsWith('\n\n')) {
|
|
173
|
+
next = raw + freshSection + '\n';
|
|
174
|
+
} else if (raw.endsWith('\n')) {
|
|
175
|
+
next = raw + '\n' + freshSection + '\n';
|
|
176
|
+
} else {
|
|
177
|
+
next = raw + '\n\n' + freshSection + '\n';
|
|
178
|
+
}
|
|
179
|
+
atomicWrite(path, next);
|
|
180
|
+
writeState(memdir, slug, { version, bodyHash: freshHash, writtenAt: new Date().toISOString() });
|
|
181
|
+
return { action: 'created' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update path: hash-guard against user edits via state sidecar.
|
|
185
|
+
if (!force) {
|
|
186
|
+
const state = readState(memdir, slug);
|
|
187
|
+
if (!state) {
|
|
188
|
+
throw new UserEditedError(
|
|
189
|
+
`${slug} sentinel found without plugin state file — treating as foreign content. Pass force=true to re-adopt.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const currentHash = sha256(match[2]);
|
|
193
|
+
if (state.bodyHash !== currentHash) {
|
|
194
|
+
throw new UserEditedError(
|
|
195
|
+
`${slug} sentinel body was modified since last write (hash mismatch). Pass force=true to overwrite.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const next = raw.replace(match[0], freshSection);
|
|
201
|
+
const changed = next !== raw;
|
|
202
|
+
if (changed) atomicWrite(path, next);
|
|
203
|
+
writeState(memdir, slug, { version, bodyHash: freshHash, writtenAt: new Date().toISOString() });
|
|
204
|
+
return { action: changed ? 'updated' : 'unchanged' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Remove the plugin's sentinel block plus its state sidecar. External content
|
|
209
|
+
* in MEMORY.md is preserved.
|
|
210
|
+
* @returns {{action: 'removed'|'absent'}}
|
|
211
|
+
*/
|
|
212
|
+
export function removePluginSection(memdir, slug) {
|
|
213
|
+
clearState(memdir, slug);
|
|
214
|
+
const path = memoryFile(memdir);
|
|
215
|
+
if (!existsSync(path)) return { action: 'absent' };
|
|
216
|
+
const raw = readFileSync(path, 'utf8');
|
|
217
|
+
const match = raw.match(sentinelRegex(slug));
|
|
218
|
+
if (!match) return { action: 'absent' };
|
|
219
|
+
|
|
220
|
+
// Delete the match plus a trailing newline + a preceding blank line so we
|
|
221
|
+
// don't leave a stranded paragraph gap.
|
|
222
|
+
let start = match.index;
|
|
223
|
+
let end = match.index + match[0].length;
|
|
224
|
+
if (raw[end] === '\n') end++;
|
|
225
|
+
if (start > 0 && raw.slice(0, start).endsWith('\n\n')) start--;
|
|
226
|
+
const next = raw.slice(0, start) + raw.slice(end);
|
|
227
|
+
atomicWrite(path, next);
|
|
228
|
+
return { action: 'removed' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether this memdir has our sentinel. Body edits don't demote the adoption —
|
|
233
|
+
* users who hand-tweak the contract line still count as adopted.
|
|
234
|
+
*/
|
|
235
|
+
export function isAdopted(memdir, slug) {
|
|
236
|
+
if (!existsSync(memdir)) return false;
|
|
237
|
+
const { section } = readMemoryIndex(memdir, slug);
|
|
238
|
+
return section !== null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Plugin detail doc IO ────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
export function writePluginDoc(memdir, slug, markdown) {
|
|
244
|
+
if (!existsSync(memdir)) mkdirSync(memdir, { recursive: true });
|
|
245
|
+
atomicWrite(docFile(memdir, slug), markdown);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function removePluginDoc(memdir, slug) {
|
|
249
|
+
const path = docFile(memdir, slug);
|
|
250
|
+
if (!existsSync(path)) return;
|
|
251
|
+
try { unlinkSync(path); } catch { /* best-effort */ }
|
|
252
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.1",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"hook-context.mjs",
|
|
36
36
|
"hook-handoff.mjs",
|
|
37
37
|
"hook-update.mjs",
|
|
38
|
+
"memdir.mjs",
|
|
39
|
+
"adopt-content.mjs",
|
|
40
|
+
"adopt-cli.mjs",
|
|
38
41
|
"haiku-client.mjs",
|
|
39
42
|
"registry.mjs",
|
|
40
43
|
"registry-retriever.mjs",
|
|
@@ -66,6 +69,8 @@
|
|
|
66
69
|
"commands/memory.md",
|
|
67
70
|
"commands/update.md",
|
|
68
71
|
"commands/tools.md",
|
|
72
|
+
"commands/adopt.md",
|
|
73
|
+
"commands/unadopt.md",
|
|
69
74
|
"hooks/hooks.json",
|
|
70
75
|
"scripts/launch.mjs",
|
|
71
76
|
"scripts/setup.sh",
|
|
@@ -85,6 +90,9 @@
|
|
|
85
90
|
"better-sqlite3": "^12.6.2",
|
|
86
91
|
"zod": "^4.3.6"
|
|
87
92
|
},
|
|
93
|
+
"overrides": {
|
|
94
|
+
"hono": ">=4.12.14"
|
|
95
|
+
},
|
|
88
96
|
"devDependencies": {
|
|
89
97
|
"@eslint/js": "^10.0.1",
|
|
90
98
|
"@vitest/coverage-v8": "^4.0.18",
|
|
@@ -167,6 +167,10 @@ function readStdin() {
|
|
|
167
167
|
|
|
168
168
|
// ─── Format Output ──────────────────────────────────────────────────────────
|
|
169
169
|
|
|
170
|
+
// Phase A (v2.31.3+): drop lesson suffix when MEM_QUIET_HOOKS=1; users on invited-memory
|
|
171
|
+
// path can mem_get the ID for full detail.
|
|
172
|
+
const QUIET_HOOKS = process.env.MEM_QUIET_HOOKS === '1';
|
|
173
|
+
|
|
170
174
|
function formatResults(rows) {
|
|
171
175
|
if (!rows || rows.length === 0) return null;
|
|
172
176
|
|
|
@@ -174,7 +178,7 @@ function formatResults(rows) {
|
|
|
174
178
|
for (const r of rows) {
|
|
175
179
|
const icon = typeIcon(r.type);
|
|
176
180
|
const title = truncate(r.title || '', 70);
|
|
177
|
-
const lesson = r.lesson_learned ? ` — ${truncate(r.lesson_learned, 50)}` : '';
|
|
181
|
+
const lesson = !QUIET_HOOKS && r.lesson_learned ? ` — ${truncate(r.lesson_learned, 50)}` : '';
|
|
178
182
|
lines.push(`#${r.id} ${icon} ${title}${lesson}`);
|
|
179
183
|
}
|
|
180
184
|
return lines.join('\n');
|
package/server-internals.mjs
CHANGED
|
@@ -5,6 +5,54 @@ import { debugCatch, COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, OBS_BM25 } from
|
|
|
5
5
|
import { BASE_STOP_WORDS } from './stop-words.mjs';
|
|
6
6
|
import { porterStem } from './tfidf.mjs';
|
|
7
7
|
|
|
8
|
+
// ─── MCP Server Instructions Builder ───────────────────────────────────────
|
|
9
|
+
// Phase A (v2.31.3+): when quiet=true, drops WHEN-TO-USE proactive-trigger and
|
|
10
|
+
// Decision-rules sections; keeps the irreducible CLI/MCP tool list. Intended
|
|
11
|
+
// for users who adopted invited-memory (MEMORY.md sentinel carries the same
|
|
12
|
+
// triggers at higher authority). Default false preserves v2.31.2 behavior.
|
|
13
|
+
|
|
14
|
+
const INSTRUCTIONS_BASE = [
|
|
15
|
+
'Long-term memory across sessions. Hooks auto-inject context; CLI preferred for explicit queries.',
|
|
16
|
+
'',
|
|
17
|
+
'CLI (via Bash):',
|
|
18
|
+
' claude-mem-lite search "query" — FTS5 full-text search',
|
|
19
|
+
' claude-mem-lite search "err" --type bugfix — filter by type',
|
|
20
|
+
' claude-mem-lite recall "file.mjs" — file-related memories',
|
|
21
|
+
' claude-mem-lite recent 5 — latest observations',
|
|
22
|
+
' claude-mem-lite get 42,43 — full details by ID',
|
|
23
|
+
' claude-mem-lite timeline --anchor 42 — chronological context',
|
|
24
|
+
'',
|
|
25
|
+
'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline for programmatic access.',
|
|
26
|
+
'mem_save: Save non-obvious insights (bugfix lessons, architecture decisions).',
|
|
27
|
+
'Search tips: short keywords (2-3 words), filter with obs_type when relevant.',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const INSTRUCTIONS_VERBOSE = [
|
|
31
|
+
'',
|
|
32
|
+
'WHEN TO USE (proactive triggers during coding):',
|
|
33
|
+
' • About to Edit/Write a file → mem_recall(file="path") FIRST — past bugfixes & lessons',
|
|
34
|
+
' • Test failure or error → mem_search(query="error keywords", obs_type="bugfix")',
|
|
35
|
+
' • Before refactoring → mem_search(query="module-name", obs_type="refactor") for past decisions',
|
|
36
|
+
' • Starting new feature → mem_search(query="feature area") for prior art & patterns',
|
|
37
|
+
' • After fixing a tricky bug → mem_save(type="bugfix", lesson_learned="root cause & fix")',
|
|
38
|
+
' • After architecture decision → mem_save(type="decision", lesson_learned="rationale")',
|
|
39
|
+
' • Hook-injected context mentions #ID → mem_get(ids=[ID]) for full details',
|
|
40
|
+
'',
|
|
41
|
+
'Decision rules (use INSTEAD OF multi-step search):',
|
|
42
|
+
' • "what happened recently?" → mem_recent (NOT search with empty query)',
|
|
43
|
+
' • "what do we know about file.mjs?" → mem_recall (NOT grep + manual search)',
|
|
44
|
+
' • "show me around observation #42" → mem_timeline (NOT mem_get + manual navigation)',
|
|
45
|
+
' • "clean up old/duplicate memories" → mem_maintain (NOT manual mem_delete loop)',
|
|
46
|
+
' • "is the search index healthy?" → mem_fts_check (NOT manual COUNT queries)',
|
|
47
|
+
' • "overview of memory tiers" → mem_browse (NOT mem_search + manual grouping)',
|
|
48
|
+
' • "export for backup" → mem_export (NOT manual SELECT queries)',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function buildServerInstructions(quiet = false) {
|
|
52
|
+
if (quiet) return INSTRUCTIONS_BASE.join('\n');
|
|
53
|
+
return [...INSTRUCTIONS_BASE, ...INSTRUCTIONS_VERBOSE].join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
8
56
|
// ─── Search Re-ranking Helpers ────────────────────────────────────────────
|
|
9
57
|
|
|
10
58
|
/**
|
package/server.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryT
|
|
|
8
8
|
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
10
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH, checkFTSIntegrity, rebuildFTS } from './schema.mjs';
|
|
11
|
-
import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup } from './server-internals.mjs';
|
|
11
|
+
import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './server-internals.mjs';
|
|
12
|
+
import { effectiveQuiet } from './hook-shared.mjs';
|
|
12
13
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
13
14
|
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
14
15
|
|
|
@@ -105,41 +106,7 @@ const RECENCY_HALF_LIFE_MS = DEFAULT_DECAY_HALF_LIFE_MS;
|
|
|
105
106
|
|
|
106
107
|
const server = new McpServer(
|
|
107
108
|
{ name: 'claude-mem-lite', version: PKG_VERSION },
|
|
108
|
-
{
|
|
109
|
-
instructions: [
|
|
110
|
-
'Long-term memory across sessions. Hooks auto-inject context; CLI preferred for explicit queries.',
|
|
111
|
-
'',
|
|
112
|
-
'CLI (via Bash):',
|
|
113
|
-
' claude-mem-lite search "query" — FTS5 full-text search',
|
|
114
|
-
' claude-mem-lite search "err" --type bugfix — filter by type',
|
|
115
|
-
' claude-mem-lite recall "file.mjs" — file-related memories',
|
|
116
|
-
' claude-mem-lite recent 5 — latest observations',
|
|
117
|
-
' claude-mem-lite get 42,43 — full details by ID',
|
|
118
|
-
' claude-mem-lite timeline --anchor 42 — chronological context',
|
|
119
|
-
'',
|
|
120
|
-
'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline for programmatic access.',
|
|
121
|
-
'mem_save: Save non-obvious insights (bugfix lessons, architecture decisions).',
|
|
122
|
-
'Search tips: short keywords (2-3 words), filter with obs_type when relevant.',
|
|
123
|
-
'',
|
|
124
|
-
'WHEN TO USE (proactive triggers during coding):',
|
|
125
|
-
' • About to Edit/Write a file → mem_recall(file="path") FIRST — past bugfixes & lessons',
|
|
126
|
-
' • Test failure or error → mem_search(query="error keywords", obs_type="bugfix")',
|
|
127
|
-
' • Before refactoring → mem_search(query="module-name", obs_type="refactor") for past decisions',
|
|
128
|
-
' • Starting new feature → mem_search(query="feature area") for prior art & patterns',
|
|
129
|
-
' • After fixing a tricky bug → mem_save(type="bugfix", lesson_learned="root cause & fix")',
|
|
130
|
-
' • After architecture decision → mem_save(type="decision", lesson_learned="rationale")',
|
|
131
|
-
' • Hook-injected context mentions #ID → mem_get(ids=[ID]) for full details',
|
|
132
|
-
'',
|
|
133
|
-
'Decision rules (use INSTEAD OF multi-step search):',
|
|
134
|
-
' • "what happened recently?" → mem_recent (NOT search with empty query)',
|
|
135
|
-
' • "what do we know about file.mjs?" → mem_recall (NOT grep + manual search)',
|
|
136
|
-
' • "show me around observation #42" → mem_timeline (NOT mem_get + manual navigation)',
|
|
137
|
-
' • "clean up old/duplicate memories" → mem_maintain (NOT manual mem_delete loop)',
|
|
138
|
-
' • "is the search index healthy?" → mem_fts_check (NOT manual COUNT queries)',
|
|
139
|
-
' • "overview of memory tiers" → mem_browse (NOT mem_search + manual grouping)',
|
|
140
|
-
' • "export for backup" → mem_export (NOT manual SELECT queries)',
|
|
141
|
-
].join('\n'),
|
|
142
|
-
},
|
|
109
|
+
{ instructions: buildServerInstructions(effectiveQuiet()) },
|
|
143
110
|
);
|
|
144
111
|
|
|
145
112
|
// Track MCP request activity for idle-time cleanup (see idle timer below)
|