claude-mem-lite 2.77.0 → 2.79.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/.mcp.json +1 -1
- package/README.md +12 -2
- package/README.zh-CN.md +12 -2
- package/commands/adopt.md +1 -1
- package/hook-update.mjs +12 -5
- package/hook.mjs +43 -4
- package/install.mjs +96 -17
- package/lib/citation-tracker.mjs +33 -0
- package/package.json +1 -1
- package/scripts/post-tool-use.sh +1 -1
- package/scripts/setup.sh +53 -13
- package/server.mjs +22 -3
- package/skip-tools.mjs +2 -1
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
@@ -138,6 +138,8 @@ The original sends **everything to the LLM and hopes it filters well**. claude-m
|
|
|
138
138
|
|
|
139
139
|
Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
|
|
140
140
|
|
|
141
|
+
> **First session per project: run `/adopt` once.** Plugin install gives you the MCP server + hooks + slash commands, but the **invited-memory sentinel** (a system-authority pointer that boosts Claude's proactive use of `mem_recall` / `mem_save`) is opt-in and project-scoped. Without it the hooks still record observations and inject context, but Claude is far less likely to call the MCP tools on its own. One-time per project: `/adopt`. Remove with `/unadopt`.
|
|
142
|
+
|
|
141
143
|
### Method 2: npx (one-liner)
|
|
142
144
|
|
|
143
145
|
```bash
|
|
@@ -159,7 +161,7 @@ Source files stay in the cloned repo. Update via `git pull && node install.mjs i
|
|
|
159
161
|
### What happens during installation
|
|
160
162
|
|
|
161
163
|
1. **Install dependencies** -- `npm install --omit=dev` (compiles native `better-sqlite3`)
|
|
162
|
-
2. **Register MCP server** -- `mem` server with 16 tools (search, recent, recall, timeline, get, save, update, stats, delete, compress, maintain, export, fts_check, browse, registry, use)
|
|
164
|
+
2. **Register MCP server** -- `mem-lite` server with 16 tools (search, recent, recall, timeline, get, save, update, stats, delete, compress, maintain, export, fts_check, browse, registry, use). The pre-v2.78 generic server name `mem` is renamed to `mem-lite` for namespace hygiene; the tool names themselves (`mem_search`, `mem_recall`, ...) are unchanged.
|
|
163
165
|
3. **Configure hooks** -- `PostToolUse`, `SessionStart`, `Stop`, `UserPromptSubmit` lifecycle hooks
|
|
164
166
|
4. **Create data directory** -- `~/.claude-mem-lite/` (hidden) for database, runtime, and managed resource files
|
|
165
167
|
5. **Auto-migrate** -- If `~/.claude-mem/` (original claude-mem) or `~/claude-mem-lite/` (pre-v0.5 unhidden) exists, migrates database and runtime files to `~/.claude-mem-lite/`, preserving the original untouched
|
|
@@ -303,7 +305,7 @@ Slash commands `/adopt` and `/unadopt` wrap the same CLI.
|
|
|
303
305
|
in the adopted project).
|
|
304
306
|
- The MCP server instructions are built once at server boot and MCP has no
|
|
305
307
|
"push" protocol — the `WHEN TO USE` / `Decision rules` trim only applies
|
|
306
|
-
after Claude Code restarts and re-spawns the mem MCP server. A single
|
|
308
|
+
after Claude Code restarts and re-spawns the mem-lite MCP server. A single
|
|
307
309
|
`/exit` + fresh session is enough. Same caveat applies to `unadopt`.
|
|
308
310
|
|
|
309
311
|
**Safety:**
|
|
@@ -511,6 +513,14 @@ Data in `~/.claude-mem-lite/` is preserved by default. Delete manually if needed
|
|
|
511
513
|
rm -rf ~/.claude-mem-lite/
|
|
512
514
|
```
|
|
513
515
|
|
|
516
|
+
### Mixed-install residue (read this if you've used multiple install methods)
|
|
517
|
+
|
|
518
|
+
`/plugin uninstall` only removes the plugin manifest — it **does not touch `~/.claude/settings.json`**. If you've ever run `claude-mem-lite install` (npx or git-clone path), hook entries pointing at `~/.claude-mem-lite/hook.mjs` were written into your user-global settings, and they keep firing after `/plugin uninstall`. If `~/.claude-mem-lite/hook.mjs` still exists they double-fire alongside the plugin; if you also ran `rm -rf ~/.claude-mem-lite/` they error every session.
|
|
519
|
+
|
|
520
|
+
**The safe sequence is**: run `claude-mem-lite uninstall` first (which cleans the settings.json hooks plus the global MCP registration), then `/plugin uninstall claude-mem-lite`, then optionally `rm -rf ~/.claude-mem-lite/`.
|
|
521
|
+
|
|
522
|
+
If you already uninstalled in the wrong order, `claude-mem-lite doctor` flags orphan hooks under `Orphan hooks:` with the exact cleanup command.
|
|
523
|
+
|
|
514
524
|
## Project Structure
|
|
515
525
|
|
|
516
526
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -146,7 +146,9 @@ node install.mjs install
|
|
|
146
146
|
### 安装过程
|
|
147
147
|
|
|
148
148
|
1. **安装依赖** -- `npm install --omit=dev`(编译原生 `better-sqlite3`)
|
|
149
|
-
2. **注册 MCP 服务器** -- `mem` 服务器,包含 15 个工具(search、recent、recall、timeline、get、save、update、stats、delete、compress、maintain、export、fts_check、browse、registry
|
|
149
|
+
2. **注册 MCP 服务器** -- `mem-lite` 服务器,包含 15 个工具(search、recent、recall、timeline、get、save、update、stats、delete、compress、maintain、export、fts_check、browse、registry)。v2.78 前服务器名为通用的 `mem`,现已改名为 `mem-lite` 避免与用户其它 `.mcp.json` 冲突;工具名(`mem_search`/`mem_recall` 等)保持不变。
|
|
150
|
+
|
|
151
|
+
> **每个项目第一次使用,跑一次 `/adopt`。** Plugin 安装给你 MCP server + hooks + slash commands,但**邀请式 memory 哨兵**(一条提升 Claude 主动调用 `mem_recall` / `mem_save` 的 system-authority 指针)是按项目 opt-in 的。不跑 `/adopt` 时 hooks 仍记录观察、注入上下文,但 Claude 不太会主动调 MCP 工具。一次性、按项目:`/adopt`;撤销 `/unadopt`。
|
|
150
152
|
3. **配置钩子** -- `PostToolUse`、`PreToolUse`、`SessionStart`、`Stop`、`UserPromptSubmit` 生命周期钩子
|
|
151
153
|
4. **创建数据目录** -- `~/.claude-mem-lite/`(隐藏目录),存放数据库、运行时和托管资源文件
|
|
152
154
|
5. **自动迁移** -- 自动检测 `~/.claude-mem/`(原版 claude-mem)或 `~/claude-mem-lite/`(v0.5 前的非隐藏目录),将数据库和运行时文件迁移到 `~/.claude-mem-lite/`,原目录保持不变
|
|
@@ -282,7 +284,7 @@ Slash 命令 `/adopt` 和 `/unadopt` 是上述 CLI 的包装。
|
|
|
282
284
|
后缀)**下一次 SessionStart** 生效(adopt 的项目下任一新会话)。
|
|
283
285
|
- MCP server instructions 是**服务启动时构建一次**,MCP 协议无 "push" 机制,
|
|
284
286
|
所以 `WHEN TO USE` / `Decision rules` 两段的瘦身**只在 Claude Code 重启后**
|
|
285
|
-
才生效(mem MCP 服务被重新 spawn)。`/exit` 一次再开新会话即可。`unadopt`
|
|
287
|
+
才生效(mem-lite MCP 服务被重新 spawn)。`/exit` 一次再开新会话即可。`unadopt`
|
|
286
288
|
同理。
|
|
287
289
|
|
|
288
290
|
**安全性:**
|
|
@@ -474,6 +476,14 @@ npx claude-mem-lite uninstall --purge
|
|
|
474
476
|
rm -rf ~/.claude-mem-lite/
|
|
475
477
|
```
|
|
476
478
|
|
|
479
|
+
### 混装残留(用过多种安装方式的话务必看一下)
|
|
480
|
+
|
|
481
|
+
`/plugin uninstall` 只删 plugin manifest,**不会动 `~/.claude/settings.json`**。如果你曾经跑过 `claude-mem-lite install`(npx 或 git-clone 路径),指向 `~/.claude-mem-lite/hook.mjs` 的 hook 条目就被写进了你的 user-global settings;`/plugin uninstall` 之后它们还在每会话触发。如果 `~/.claude-mem-lite/hook.mjs` 还在 → 与 plugin 双触发;如果你又 `rm -rf ~/.claude-mem-lite/` → 每次会话报错。
|
|
482
|
+
|
|
483
|
+
**正确顺序**:先 `claude-mem-lite uninstall`(清 settings.json 的 hook + 全局 MCP 注册),再 `/plugin uninstall claude-mem-lite`,最后可选 `rm -rf ~/.claude-mem-lite/`。
|
|
484
|
+
|
|
485
|
+
顺序搞错了的话,`claude-mem-lite doctor` 会在 `Orphan hooks:` 一节标出残留并给清理命令。
|
|
486
|
+
|
|
477
487
|
## 项目结构
|
|
478
488
|
|
|
479
489
|
```
|
package/commands/adopt.md
CHANGED
|
@@ -52,7 +52,7 @@ session. After running `adopt`:
|
|
|
52
52
|
on the next SessionStart.
|
|
53
53
|
- MCP-instructions trim (`WHEN TO USE` / `Decision rules` sections) only
|
|
54
54
|
takes effect after Claude Code itself restarts (or at least re-attaches
|
|
55
|
-
the mem MCP server). If you still see the verbose MCP instructions after
|
|
55
|
+
the mem-lite MCP server). If you still see the verbose MCP instructions after
|
|
56
56
|
adopt, a `/exit` + fresh session is enough.
|
|
57
57
|
|
|
58
58
|
Same caveat applies in reverse for `/unadopt`.
|
package/hook-update.mjs
CHANGED
|
@@ -311,16 +311,23 @@ export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
|
|
|
311
311
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
312
312
|
rmSync(backupDir, { recursive: true, force: true });
|
|
313
313
|
|
|
314
|
-
// Post-update migration: clean stale global
|
|
314
|
+
// Post-update migration: clean stale global MCPs if plugin handles it.
|
|
315
|
+
// Both "mem" (legacy, pre-v2.78) and "mem-lite" (current) are purged so a
|
|
316
|
+
// user who manually ran `claude mcp add` in either era doesn't end up with
|
|
317
|
+
// duplicate global + plugin registrations after the rename.
|
|
315
318
|
try {
|
|
316
319
|
if (isPluginMode()) {
|
|
317
320
|
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
318
321
|
const cfg = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
let changed = false;
|
|
323
|
+
for (const k of ['mem', 'mem-lite']) {
|
|
324
|
+
if (cfg.mcpServers?.[k]) {
|
|
325
|
+
delete cfg.mcpServers[k];
|
|
326
|
+
changed = true;
|
|
327
|
+
debugLog('DEBUG', 'hook-update', `Post-update: removed stale global MCP "${k}"`);
|
|
328
|
+
}
|
|
323
329
|
}
|
|
330
|
+
if (changed) writeFileSync(claudeJsonPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
324
331
|
}
|
|
325
332
|
} catch (e) { debugCatch(e, 'post-update-mcp-dedup'); }
|
|
326
333
|
|
package/hook.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { randomUUID } from 'crypto';
|
|
22
22
|
import { join } from 'path';
|
|
23
|
-
import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statSync } from 'fs';
|
|
23
|
+
import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statSync, existsSync } from 'fs';
|
|
24
24
|
import { homedir } from 'os';
|
|
25
25
|
import {
|
|
26
26
|
truncate, inferProject, detectBashSignificance,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
bumpCitationAccess,
|
|
52
52
|
computeCiteRecall,
|
|
53
53
|
applyCitationDecay,
|
|
54
|
+
hasMainThreadAssistantText,
|
|
54
55
|
} from './lib/citation-tracker.mjs';
|
|
55
56
|
import { extractTailAssistantText, extractStructuredSummary } from './lib/summary-extractor.mjs';
|
|
56
57
|
import { searchRelevantMemories, formatMemoryLine } from './hook-memory.mjs';
|
|
@@ -537,9 +538,20 @@ async function handleStop() {
|
|
|
537
538
|
try {
|
|
538
539
|
const injected = extractAllInjected(transcriptPath);
|
|
539
540
|
if (injected.size > 0) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
541
|
+
// Text-floor gate: skip decay on tool-only Stops. Without this,
|
|
542
|
+
// a turn that ends on tool_use locks every injected obs as
|
|
543
|
+
// uncited (last_decided_session_id set), so a later turn that
|
|
544
|
+
// cites correctly can't undo the verdict. Per CLAUDE.md the
|
|
545
|
+
// contract is "NEXT time you produce user-facing text," so a
|
|
546
|
+
// session with zero main-thread text gets a free pass — the
|
|
547
|
+
// next Stop in the same session will re-evaluate.
|
|
548
|
+
if (!hasMainThreadAssistantText(transcriptPath)) {
|
|
549
|
+
debugLog('DEBUG', 'handleStop', `citation-decay: skipped (no main-thread assistant text yet, injected=${injected.size})`);
|
|
550
|
+
} else {
|
|
551
|
+
const citedMain = extractCitationsFromTranscript(transcriptPath, { mainOnly: true });
|
|
552
|
+
const r = applyCitationDecay(db, project, injected, citedMain, sessionId);
|
|
553
|
+
debugLog('DEBUG', 'handleStop', `citation-decay: touched=${r.touched} promoted=${r.promoted} demoted=${r.demoted}`);
|
|
554
|
+
}
|
|
543
555
|
}
|
|
544
556
|
} catch (e) { debugCatch(e, 'handleStop-citation-decay'); }
|
|
545
557
|
|
|
@@ -1103,6 +1115,33 @@ async function handleSessionStart() {
|
|
|
1103
1115
|
if (citeNudge) {
|
|
1104
1116
|
dashboardText = dashboardText ? `${citeNudge}\n${dashboardText}` : citeNudge;
|
|
1105
1117
|
}
|
|
1118
|
+
// v2.79: surface setup.sh dependency-install failure as a high-visibility
|
|
1119
|
+
// line at the very top of the dashboard. setup.sh writes runtime/.deps-broken
|
|
1120
|
+
// (JSON: ts/reason/root/repair) on failure and removes it on success — so
|
|
1121
|
+
// a stale flag self-heals on the next clean SessionStart. Without this
|
|
1122
|
+
// surface, hook degradation looks identical to "nothing happening" until
|
|
1123
|
+
// the user notices missing context days later.
|
|
1124
|
+
try {
|
|
1125
|
+
const depsFlag = join(RUNTIME_DIR, '.deps-broken');
|
|
1126
|
+
if (existsSync(depsFlag)) {
|
|
1127
|
+
let detail = 'unknown';
|
|
1128
|
+
let repair = '';
|
|
1129
|
+
try {
|
|
1130
|
+
const raw = readFileSync(depsFlag, 'utf8').trim();
|
|
1131
|
+
const parsed = JSON.parse(raw);
|
|
1132
|
+
detail = parsed.reason || detail;
|
|
1133
|
+
repair = parsed.repair || '';
|
|
1134
|
+
} catch { /* corrupt flag — surface the fact only */ }
|
|
1135
|
+
const nudgeLines = [
|
|
1136
|
+
'⚠️ [claude-mem-lite] Hook dependencies failed to install on the last SessionStart.',
|
|
1137
|
+
` Reason: ${detail}`,
|
|
1138
|
+
];
|
|
1139
|
+
if (repair) nudgeLines.push(` Repair: ${repair}`);
|
|
1140
|
+
nudgeLines.push(' Until fixed, PreToolUse / PostToolUse / memory injection are degraded.');
|
|
1141
|
+
const nudge = nudgeLines.join('\n');
|
|
1142
|
+
dashboardText = dashboardText ? `${nudge}\n${dashboardText}` : nudge;
|
|
1143
|
+
}
|
|
1144
|
+
} catch (e) { debugCatch(e, 'session-start-deps-flag'); }
|
|
1106
1145
|
if (dashboardText) {
|
|
1107
1146
|
process.stdout.write(JSON.stringify({
|
|
1108
1147
|
suppressOutput: true,
|
package/install.mjs
CHANGED
|
@@ -428,10 +428,12 @@ async function install() {
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
// 3. Register MCP server (skip if plugin system already handles it)
|
|
431
|
-
// Plugin MCP must stay at root .mcp.json so Claude Code registers plugin:*:mem.
|
|
432
|
-
// Duplicate
|
|
431
|
+
// Plugin MCP must stay at root .mcp.json so Claude Code registers plugin:*:mem-lite.
|
|
432
|
+
// Duplicate registrations in practice come from old global install.mjs state
|
|
433
433
|
// (claude mcp add) or stale marketplace copies, not from the cache root itself.
|
|
434
|
-
// Global registration via `claude mcp add` creates a DUPLICATE
|
|
434
|
+
// Global registration via `claude mcp add` creates a DUPLICATE mcp__mem-lite__* server.
|
|
435
|
+
// The legacy generic name "mem" (pre-v2.78) is also purged so a user who installed in
|
|
436
|
+
// either era ends up with a single canonical "mem-lite" registration.
|
|
435
437
|
// Detect plugin mode: installed_plugins.json has our entry → plugin handles MCP.
|
|
436
438
|
const installedPluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
437
439
|
let pluginHandlesMcp = false;
|
|
@@ -442,18 +444,26 @@ async function install() {
|
|
|
442
444
|
|
|
443
445
|
if (pluginHandlesMcp) {
|
|
444
446
|
log('MCP server: plugin system handles registration (skipping global)');
|
|
445
|
-
// Clean up stale global
|
|
446
|
-
|
|
447
|
+
// Clean up stale global registrations (both legacy "mem" and current "mem-lite")
|
|
448
|
+
for (const name of ['mem', 'mem-lite']) {
|
|
449
|
+
try {
|
|
450
|
+
execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' });
|
|
451
|
+
ok(`Removed stale global MCP "${name}"`);
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
447
454
|
} else {
|
|
448
455
|
log('Registering MCP server...');
|
|
449
456
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
457
|
+
// Purge legacy "mem" and any pre-existing "mem-lite" before re-registering
|
|
458
|
+
for (const name of ['mem', 'mem-lite']) {
|
|
459
|
+
try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' }); } catch {}
|
|
460
|
+
try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', name], { stdio: 'pipe' }); } catch {}
|
|
461
|
+
}
|
|
462
|
+
execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem-lite', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
|
|
463
|
+
ok('MCP server registered: mem-lite');
|
|
454
464
|
} catch (e) {
|
|
455
465
|
fail('MCP registration failed: ' + e.message);
|
|
456
|
-
warn('Try manually: claude mcp add -s user -t stdio mem -- node ' + SERVER_PATH);
|
|
466
|
+
warn('Try manually: claude mcp add -s user -t stdio mem-lite -- node ' + SERVER_PATH);
|
|
457
467
|
}
|
|
458
468
|
}
|
|
459
469
|
|
|
@@ -933,13 +943,18 @@ async function install() {
|
|
|
933
943
|
async function uninstall() {
|
|
934
944
|
console.log('\nclaude-mem-lite uninstaller\n');
|
|
935
945
|
|
|
936
|
-
// 1. Remove MCP (legacy hook-based install)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
946
|
+
// 1. Remove MCP (legacy hook-based install).
|
|
947
|
+
// Try both the legacy "mem" (pre-v2.78) and current "mem-lite" names so a user
|
|
948
|
+
// who installed in either era ends up clean.
|
|
949
|
+
let removedAny = false;
|
|
950
|
+
for (const name of ['mem', 'mem-lite']) {
|
|
951
|
+
try {
|
|
952
|
+
execFileSync('claude', ['mcp', 'remove', '-s', 'user', name], { stdio: 'pipe' });
|
|
953
|
+
ok(`MCP server removed: ${name}`);
|
|
954
|
+
removedAny = true;
|
|
955
|
+
} catch {}
|
|
942
956
|
}
|
|
957
|
+
if (!removedAny) warn('MCP server not found or already removed');
|
|
943
958
|
|
|
944
959
|
// 1b. Remove CLI symlink
|
|
945
960
|
for (const binDir of [join(homedir(), '.local', 'bin'), '/usr/local/bin']) {
|
|
@@ -1068,7 +1083,11 @@ async function status() {
|
|
|
1068
1083
|
// MCP
|
|
1069
1084
|
try {
|
|
1070
1085
|
const list = execFileSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
1071
|
-
|
|
1086
|
+
// Accept either the current "mem-lite" registration or the legacy "mem"
|
|
1087
|
+
// name (pre-v2.78) so a user mid-upgrade still sees a green status until
|
|
1088
|
+
// setup.sh / install.mjs purges the legacy entry on next run.
|
|
1089
|
+
const registered = list.includes('mem-lite:') || list.includes('mem-lite ')
|
|
1090
|
+
|| list.includes('mem:') || /\bmem\b\s/.test(list);
|
|
1072
1091
|
push(registered ? 'ok' : 'fail', 'mcp', registered ? 'MCP server: registered' : 'MCP server: not registered', { registered });
|
|
1073
1092
|
} catch {
|
|
1074
1093
|
push('warn', 'mcp', 'Could not check MCP status', { registered: null });
|
|
@@ -1242,6 +1261,24 @@ async function doctor() {
|
|
|
1242
1261
|
dwarn('Plugin lifecycle: hooks not configured');
|
|
1243
1262
|
}
|
|
1244
1263
|
|
|
1264
|
+
// Orphan hooks: settings.json entries referencing hook files that no longer
|
|
1265
|
+
// exist on disk. Trips when a user runs `/plugin uninstall` and/or
|
|
1266
|
+
// `rm -rf ~/.claude-mem-lite/` without first running `claude-mem-lite uninstall`
|
|
1267
|
+
// (which clears the settings.json entries). The hooks keep firing and exit
|
|
1268
|
+
// with require-error noise every session. README's Uninstall section warns
|
|
1269
|
+
// about the right ordering; this check flags the broken state so it surfaces
|
|
1270
|
+
// even when the user skipped the README.
|
|
1271
|
+
const orphanPaths = collectOrphanHookPaths(settings);
|
|
1272
|
+
if (orphanPaths.length > 0) {
|
|
1273
|
+
fail(`Orphan hooks: ${orphanPaths.length} settings.json entr${orphanPaths.length === 1 ? 'y references a' : 'ies reference'} missing file(s)`);
|
|
1274
|
+
for (const p of orphanPaths.slice(0, 5)) log(` missing: ${p}`);
|
|
1275
|
+
if (orphanPaths.length > 5) log(` ... +${orphanPaths.length - 5} more`);
|
|
1276
|
+
log(` Repair: node ${join(PROJECT_DIR, 'install.mjs')} uninstall # removes the dead hook entries`);
|
|
1277
|
+
issues++;
|
|
1278
|
+
} else if (hasHooks) {
|
|
1279
|
+
ok('Orphan hooks: none (all hook targets present)');
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1245
1282
|
// Database
|
|
1246
1283
|
if (existsSync(DB_PATH)) {
|
|
1247
1284
|
try {
|
|
@@ -1464,6 +1501,48 @@ function hasMemHooksConfigured(settings) {
|
|
|
1464
1501
|
);
|
|
1465
1502
|
}
|
|
1466
1503
|
|
|
1504
|
+
/**
|
|
1505
|
+
* Walk every mem-hook command in settings.json and collect any absolute file
|
|
1506
|
+
* paths that don't currently exist on disk. Used by doctor() to surface
|
|
1507
|
+
* post-uninstall residue ("/plugin uninstall claude-mem-lite" leaves
|
|
1508
|
+
* settings.json hooks pointing at ~/.claude-mem-lite/hook.mjs; if the user
|
|
1509
|
+
* then deleted that directory, every session start dispatches to a missing
|
|
1510
|
+
* file).
|
|
1511
|
+
*
|
|
1512
|
+
* Path extraction: command strings look like:
|
|
1513
|
+
* node "/home/sds/.claude-mem-lite/hook.mjs" session-start
|
|
1514
|
+
* bash "/home/sds/.claude-mem-lite/scripts/post-tool-use.sh"
|
|
1515
|
+
* node "/home/sds/.claude-mem-lite/scripts/pre-tool-recall.js"
|
|
1516
|
+
* We pick the first quoted absolute path; if there is no quoted token we fall
|
|
1517
|
+
* back to the first whitespace-delimited absolute-looking token after the
|
|
1518
|
+
* interpreter. ${CLAUDE_PLUGIN_ROOT}-templated commands are ignored — those
|
|
1519
|
+
* are plugin-owned hooks resolved by Claude Code at runtime, not by us.
|
|
1520
|
+
*/
|
|
1521
|
+
export function collectOrphanHookPaths(settings) {
|
|
1522
|
+
if (!settings?.hooks) return [];
|
|
1523
|
+
const out = [];
|
|
1524
|
+
for (const configs of Object.values(settings.hooks)) {
|
|
1525
|
+
if (!Array.isArray(configs)) continue;
|
|
1526
|
+
for (const cfg of configs) {
|
|
1527
|
+
if (!isMemHook(cfg)) continue;
|
|
1528
|
+
for (const h of cfg.hooks || []) {
|
|
1529
|
+
const cmd = h.command || '';
|
|
1530
|
+
if (cmd.includes('${CLAUDE_PLUGIN_ROOT}')) continue;
|
|
1531
|
+
const quoted = cmd.match(/"([^"]+)"/);
|
|
1532
|
+
let path = quoted ? quoted[1] : null;
|
|
1533
|
+
if (!path) {
|
|
1534
|
+
// unquoted: split on whitespace, take the first arg that looks like an absolute path
|
|
1535
|
+
const parts = cmd.split(/\s+/);
|
|
1536
|
+
path = parts.find(p => p.startsWith('/') && (p.endsWith('.mjs') || p.endsWith('.js') || p.endsWith('.sh'))) || null;
|
|
1537
|
+
}
|
|
1538
|
+
if (!path) continue;
|
|
1539
|
+
if (!existsSync(path) && !out.includes(path)) out.push(path);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return out;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1467
1546
|
/**
|
|
1468
1547
|
* v2.48 P1-4: prune top-level stale files left behind by removed-module upgrades.
|
|
1469
1548
|
*
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -266,6 +266,39 @@ export function extractAllInjected(transcriptPath) {
|
|
|
266
266
|
]);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* True iff the transcript contains at least one non-whitespace text block from
|
|
271
|
+
* a main-thread assistant turn. Gates the citation-decay loop so a tool-only
|
|
272
|
+
* Stop doesn't lock an injection as "uncited" before the model has had a
|
|
273
|
+
* chance to produce user-facing text. Per CLAUDE.md the cite contract is
|
|
274
|
+
* "NEXT time you produce user-facing text" — not "same turn." Without this
|
|
275
|
+
* gate, a turn that ends on tool_use sees applyCitationDecay run, set
|
|
276
|
+
* last_decided_session_id, and freeze the verdict at uncited even though a
|
|
277
|
+
* later turn in the same session would have cited correctly.
|
|
278
|
+
*
|
|
279
|
+
* @param {string|null|undefined} transcriptPath
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
export function hasMainThreadAssistantText(transcriptPath) {
|
|
283
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return false;
|
|
284
|
+
let raw;
|
|
285
|
+
try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return false; }
|
|
286
|
+
for (const line of raw.split('\n')) {
|
|
287
|
+
if (!line.trim()) continue;
|
|
288
|
+
let entry;
|
|
289
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
290
|
+
if (entry.type !== 'assistant' || !entry.message) continue;
|
|
291
|
+
if (entry.isSidechain === true) continue;
|
|
292
|
+
const content = entry.message.content;
|
|
293
|
+
if (!Array.isArray(content)) continue;
|
|
294
|
+
for (const block of content) {
|
|
295
|
+
if (block.type !== 'text' || typeof block.text !== 'string') continue;
|
|
296
|
+
if (block.text.trim().length > 0) return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
269
302
|
const IMPORTANCE_CAP = 3;
|
|
270
303
|
const IMPORTANCE_FLOOR = 0;
|
|
271
304
|
const UNCITED_STREAK_THRESHOLD = 3;
|
package/package.json
CHANGED
package/scripts/post-tool-use.sh
CHANGED
|
@@ -51,7 +51,7 @@ case "$tool" in
|
|
|
51
51
|
exit 0
|
|
52
52
|
;;
|
|
53
53
|
# Prefix filters
|
|
54
|
-
mem_*|mcp__mem__*|mcp__plugin_claude-mem-lite*|mcp__sequential*|mcp__plugin_context7*)
|
|
54
|
+
mem_*|mcp__mem__*|mcp__mem-lite__*|mcp__plugin_claude-mem-lite*|mcp__sequential*|mcp__plugin_context7*)
|
|
55
55
|
exit 0
|
|
56
56
|
;;
|
|
57
57
|
esac
|
package/scripts/setup.sh
CHANGED
|
@@ -69,11 +69,39 @@ mkdir -p "$DATA_DIR/runtime"
|
|
|
69
69
|
|
|
70
70
|
# 6. Ensure native dependencies available for hooks (ESM import needs node_modules in resolution chain)
|
|
71
71
|
# Plugin cache doesn't include node_modules — symlink from data dir or npm install on first run
|
|
72
|
+
#
|
|
73
|
+
# Visibility contract: `npm install` failure here used to be a stderr-only log_warn,
|
|
74
|
+
# invisible to the Claude session unless the operator was watching the terminal.
|
|
75
|
+
# When it fails (no toolchain, blocked network, read-only FS) every hook silently
|
|
76
|
+
# degrades — pre-tool-recall, post-tool-use, session-start all import better-sqlite3
|
|
77
|
+
# and exit on the require() error. v2.79: write a JSON flag to runtime/.deps-broken
|
|
78
|
+
# and hook.mjs SessionStart surfaces it in the Claude context as a HIGH-VISIBILITY
|
|
79
|
+
# block; success branches remove the flag so a self-heal stays visible too.
|
|
80
|
+
DEPS_FLAG="$DATA_DIR/runtime/.deps-broken"
|
|
81
|
+
mkdir -p "$DATA_DIR/runtime" 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
mark_deps_broken() {
|
|
84
|
+
local reason="$1"
|
|
85
|
+
# Embed reason + repair command so hook.mjs renders a complete error without
|
|
86
|
+
# having to re-derive them. Single-line JSON for trivial parse.
|
|
87
|
+
printf '{"ts":"%s","reason":%s,"root":%s,"repair":%s}\n' \
|
|
88
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
89
|
+
"\"$reason\"" \
|
|
90
|
+
"\"$ROOT\"" \
|
|
91
|
+
"\"cd '$ROOT' && npm install --omit=dev\"" \
|
|
92
|
+
> "$DEPS_FLAG" 2>/dev/null || true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mark_deps_ok() {
|
|
96
|
+
rm -f "$DEPS_FLAG" 2>/dev/null || true
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
73
100
|
# Fast path: symlink from data dir (instant, no network needed)
|
|
74
101
|
if [[ -d "$DATA_DIR/node_modules/better-sqlite3" ]]; then
|
|
75
102
|
if ln -sfn "$DATA_DIR/node_modules" "$ROOT/node_modules" 2>/dev/null; then
|
|
76
103
|
log_ok "Dependencies linked from $DATA_DIR"
|
|
104
|
+
mark_deps_ok
|
|
77
105
|
fi
|
|
78
106
|
fi
|
|
79
107
|
# Slow path: npm install (first-time only, ~10-20s for native addon)
|
|
@@ -81,35 +109,47 @@ if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
|
81
109
|
log_info "Installing dependencies (first-time setup)..."
|
|
82
110
|
if (cd "$ROOT" && npm install --omit=dev --no-audit --no-fund 2>&1) >&2; then
|
|
83
111
|
log_ok "Dependencies installed"
|
|
112
|
+
mark_deps_ok
|
|
84
113
|
else
|
|
85
|
-
log_warn "Dependency install failed — hooks may have limited functionality"
|
|
114
|
+
log_warn "Dependency install failed — hooks may have limited functionality (flag: $DEPS_FLAG)"
|
|
115
|
+
mark_deps_broken "npm install --omit=dev failed in plugin cache root"
|
|
86
116
|
fi
|
|
87
117
|
fi
|
|
118
|
+
else
|
|
119
|
+
# Deps already present — make sure we don't keep stale broken flag around
|
|
120
|
+
mark_deps_ok
|
|
88
121
|
fi
|
|
89
122
|
|
|
90
|
-
# 7. MCP cleanup: idempotently clean stale registrations from
|
|
91
|
-
# This runs on every plugin SessionStart because old global
|
|
123
|
+
# 7. MCP cleanup: idempotently clean stale registrations from older installs.
|
|
124
|
+
# This runs on every plugin SessionStart because old global entries may still
|
|
92
125
|
# exist even after an earlier migration marker was written.
|
|
93
|
-
#
|
|
94
|
-
# -
|
|
95
|
-
#
|
|
126
|
+
# - Pre-2.10: direct installs left a global "mem" MCP alongside plugin MCP.
|
|
127
|
+
# - Pre-2.78: plugin and global registrations used the generic name "mem";
|
|
128
|
+
# v2.78 renamed to "mem-lite" — any stale global "mem" must be purged so
|
|
129
|
+
# Claude Code doesn't surface duplicate (old "mem" + new "mem-lite") tool
|
|
130
|
+
# prefixes side-by-side.
|
|
96
131
|
# Root .mcp.json in the installed plugin cache is required for Claude Code to
|
|
97
132
|
# register plugin MCP; only stale global/marketplace copies should be removed.
|
|
98
|
-
MCP_MIGRATION="$DATA_DIR/runtime/.mcp-dedup-v2.
|
|
133
|
+
MCP_MIGRATION="$DATA_DIR/runtime/.mcp-dedup-v2.78"
|
|
99
134
|
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
|
|
100
135
|
CLAUDE_JSON="$HOME/.claude.json" node -e '
|
|
101
136
|
const fs = require("fs");
|
|
102
137
|
let changed = false;
|
|
103
|
-
// Remove stale global MCP
|
|
138
|
+
// Remove stale global MCP registrations (plugin .mcp.json handles it).
|
|
139
|
+
// Both "mem" (legacy, pre-v2.78) and "mem-lite" (current) are purged from
|
|
140
|
+
// user-global scope when running inside the plugin — the plugin manifest
|
|
141
|
+
// is the single source of truth.
|
|
104
142
|
try {
|
|
105
143
|
const p = process.env.CLAUDE_JSON;
|
|
106
144
|
const d = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
for (const k of ["mem", "mem-lite"]) {
|
|
146
|
+
if (d.mcpServers?.[k]) {
|
|
147
|
+
delete d.mcpServers[k];
|
|
148
|
+
process.stderr.write(`✓ Removed stale global MCP "${k}" (plugin handles it)\n`);
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
112
151
|
}
|
|
152
|
+
if (changed) fs.writeFileSync(p, JSON.stringify(d, null, 2) + "\n");
|
|
113
153
|
} catch {}
|
|
114
154
|
// NOTE: Do NOT touch marketplace .mcp.json — Claude Code copies it from
|
|
115
155
|
// marketplace → plugin cache on updates. Clearing it causes the cache
|
package/server.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
|
|
|
12
12
|
import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './server-internals.mjs';
|
|
13
13
|
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
14
14
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
15
|
-
import { effectiveQuiet } from './hook-shared.mjs';
|
|
15
|
+
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
16
16
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
17
17
|
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
18
18
|
|
|
@@ -43,7 +43,7 @@ const { version: PKG_VERSION } = require('./package.json');
|
|
|
43
43
|
|
|
44
44
|
// ─── Database ───────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
-
import { rmSync, existsSync, readFileSync } from 'fs';
|
|
46
|
+
import { rmSync, existsSync, readFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
47
47
|
|
|
48
48
|
let db;
|
|
49
49
|
try {
|
|
@@ -127,7 +127,7 @@ if (process.env.CLAUDE_MEM_QUIET_TRACE !== '0') {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const server = new McpServer(
|
|
130
|
-
{ name: '
|
|
130
|
+
{ name: 'mem-lite', version: PKG_VERSION },
|
|
131
131
|
{ instructions: buildServerInstructions(_quiet) },
|
|
132
132
|
);
|
|
133
133
|
|
|
@@ -2267,5 +2267,24 @@ process.on('unhandledRejection', (err) => { debugCatch(err, 'unhandledRejection'
|
|
|
2267
2267
|
|
|
2268
2268
|
// ─── Start Server ───────────────────────────────────────────────────────────
|
|
2269
2269
|
|
|
2270
|
+
// Spawn telemetry — appends one JSON line per process start so we can diagnose
|
|
2271
|
+
// dual-registration (plugin namespace + local .mcp.json could both spawn the
|
|
2272
|
+
// server in one Claude Code session). Two records with close timestamps and
|
|
2273
|
+
// the same ppid is the smoking gun. Never throws — telemetry must not block
|
|
2274
|
+
// startup. Disable with MEM_DISABLE_SPAWN_LOG=1.
|
|
2275
|
+
if (process.env.MEM_DISABLE_SPAWN_LOG !== '1') {
|
|
2276
|
+
try {
|
|
2277
|
+
if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
2278
|
+
const line = JSON.stringify({
|
|
2279
|
+
ts: new Date().toISOString(),
|
|
2280
|
+
pid: process.pid,
|
|
2281
|
+
ppid: process.ppid,
|
|
2282
|
+
argv1: process.argv[1] || '',
|
|
2283
|
+
version: PKG_VERSION,
|
|
2284
|
+
}) + '\n';
|
|
2285
|
+
appendFileSync(join(RUNTIME_DIR, 'mcp-spawns.log'), line, { mode: 0o600 });
|
|
2286
|
+
} catch { /* never block startup on telemetry failure */ }
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2270
2289
|
const transport = new StdioServerTransport();
|
|
2271
2290
|
await server.connect(transport);
|
package/skip-tools.mjs
CHANGED
|
@@ -14,7 +14,8 @@ export const SKIP_TOOLS = new Set([
|
|
|
14
14
|
/** Prefix patterns — tools starting with these are also skipped */
|
|
15
15
|
export const SKIP_PREFIXES = [
|
|
16
16
|
'mem_',
|
|
17
|
-
'mcp__mem__',
|
|
17
|
+
'mcp__mem__', // legacy global MCP name (pre-rename)
|
|
18
|
+
'mcp__mem-lite__', // current global MCP name (post-rename v2.78+)
|
|
18
19
|
'mcp__plugin_claude-mem-lite',
|
|
19
20
|
'mcp__sequential',
|
|
20
21
|
'mcp__plugin_context7',
|