claude-mem-lite 2.79.0 → 2.80.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.79.0",
13
+ "version": "2.80.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.79.0",
3
+ "version": "2.80.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/README.md CHANGED
@@ -161,7 +161,7 @@ Source files stay in the cloned repo. Update via `git pull && node install.mjs i
161
161
  ### What happens during installation
162
162
 
163
163
  1. **Install dependencies** -- `npm install --omit=dev` (compiles native `better-sqlite3`)
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.
164
+ 2. **Register MCP server** -- `mem-lite` server with 20 tools (9 core exposed via `tools/list` + 11 hidden-but-callable; see the Usage section for the full table). 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.
165
165
  3. **Configure hooks** -- `PostToolUse`, `SessionStart`, `Stop`, `UserPromptSubmit` lifecycle hooks
166
166
  4. **Create data directory** -- `~/.claude-mem-lite/` (hidden) for database, runtime, and managed resource files
167
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
package/README.zh-CN.md CHANGED
@@ -146,7 +146,7 @@ node install.mjs install
146
146
  ### 安装过程
147
147
 
148
148
  1. **安装依赖** -- `npm install --omit=dev`(编译原生 `better-sqlite3`)
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` 等)保持不变。
149
+ 2. **注册 MCP 服务器** -- `mem-lite` 服务器,包含 20 个工具(9 个核心通过 `tools/list` 暴露 + 11 个隐藏但可调;完整表见 Usage 段)。v2.78 前服务器名为通用的 `mem`,现已改名为 `mem-lite` 避免与用户其它 `.mcp.json` 冲突;工具名(`mem_search`/`mem_recall` 等)保持不变。
150
150
 
151
151
  > **每个项目第一次使用,跑一次 `/adopt`。** Plugin 安装给你 MCP server + hooks + slash commands,但**邀请式 memory 哨兵**(一条提升 Claude 主动调用 `mem_recall` / `mem_save` 的 system-authority 指针)是按项目 opt-in 的。不跑 `/adopt` 时 hooks 仍记录观察、注入上下文,但 Claude 不太会主动调 MCP 工具。一次性、按项目:`/adopt`;撤销 `/unadopt`。
152
152
  3. **配置钩子** -- `PostToolUse`、`PreToolUse`、`SessionStart`、`Stop`、`UserPromptSubmit` 生命周期钩子
package/install.mjs CHANGED
@@ -1086,8 +1086,12 @@ async function status() {
1086
1086
  // Accept either the current "mem-lite" registration or the legacy "mem"
1087
1087
  // name (pre-v2.78) so a user mid-upgrade still sees a green status until
1088
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);
1089
+ // v2.79.1: dropped a `/\bmem\b\s/` fallback regex — the `\b` word boundary
1090
+ // also matched "mem-lite" (because `-` is a non-word char), so the regex
1091
+ // was always-true noise (benign only because the mem-lite checks short-
1092
+ // circuited first). `claude mcp list` formats as `<name>: <command>`, so
1093
+ // the two colon-form checks below cover every shape.
1094
+ const registered = list.includes('mem-lite:') || list.includes('mem:');
1091
1095
  push(registered ? 'ok' : 'fail', 'mcp', registered ? 'MCP server: registered' : 'MCP server: not registered', { registered });
1092
1096
  } catch {
1093
1097
  push('warn', 'mcp', 'Could not check MCP status', { registered: null });
@@ -1270,7 +1274,7 @@ async function doctor() {
1270
1274
  // even when the user skipped the README.
1271
1275
  const orphanPaths = collectOrphanHookPaths(settings);
1272
1276
  if (orphanPaths.length > 0) {
1273
- fail(`Orphan hooks: ${orphanPaths.length} settings.json entr${orphanPaths.length === 1 ? 'y references a' : 'ies reference'} missing file(s)`);
1277
+ fail(`Orphan hooks: ${orphanPaths.length} settings.json entr${orphanPaths.length === 1 ? 'y references a missing file' : 'ies reference missing files'}`);
1274
1278
  for (const p of orphanPaths.slice(0, 5)) log(` missing: ${p}`);
1275
1279
  if (orphanPaths.length > 5) log(` ... +${orphanPaths.length - 5} more`);
1276
1280
  log(` Repair: node ${join(PROJECT_DIR, 'install.mjs')} uninstall # removes the dead hook entries`);
@@ -1518,6 +1522,13 @@ function hasMemHooksConfigured(settings) {
1518
1522
  * interpreter. ${CLAUDE_PLUGIN_ROOT}-templated commands are ignored — those
1519
1523
  * are plugin-owned hooks resolved by Claude Code at runtime, not by us.
1520
1524
  */
1525
+ const HOOK_PATH_EXTS = ['.mjs', '.js', '.cjs', '.sh'];
1526
+
1527
+ function looksLikeHookPath(p) {
1528
+ if (!p || !p.startsWith('/')) return false;
1529
+ return HOOK_PATH_EXTS.some(ext => p.endsWith(ext));
1530
+ }
1531
+
1521
1532
  export function collectOrphanHookPaths(settings) {
1522
1533
  if (!settings?.hooks) return [];
1523
1534
  const out = [];
@@ -1528,12 +1539,19 @@ export function collectOrphanHookPaths(settings) {
1528
1539
  for (const h of cfg.hooks || []) {
1529
1540
  const cmd = h.command || '';
1530
1541
  if (cmd.includes('${CLAUDE_PLUGIN_ROOT}')) continue;
1531
- const quoted = cmd.match(/"([^"]+)"/);
1532
- let path = quoted ? quoted[1] : null;
1542
+ // v2.80: scan ALL quoted tokens (was: only the first), prefer ones
1543
+ // that look like a hook path. Fixes a footgun where a wrapper command
1544
+ // like `bash -c "some inline" "/real/path.sh"` would pick "some inline"
1545
+ // and flag a false orphan. If no quoted token looks like a path, fall
1546
+ // through to the unquoted scanner; if that also misses, skip the
1547
+ // entry — we'd rather under-report than false-flag.
1548
+ let path = null;
1549
+ for (const m of cmd.matchAll(/"([^"]+)"/g)) {
1550
+ if (looksLikeHookPath(m[1])) { path = m[1]; break; }
1551
+ }
1533
1552
  if (!path) {
1534
- // unquoted: split on whitespace, take the first arg that looks like an absolute path
1535
1553
  const parts = cmd.split(/\s+/);
1536
- path = parts.find(p => p.startsWith('/') && (p.endsWith('.mjs') || p.endsWith('.js') || p.endsWith('.sh'))) || null;
1554
+ path = parts.find(p => looksLikeHookPath(p)) || null;
1537
1555
  }
1538
1556
  if (!path) continue;
1539
1557
  if (!existsSync(path) && !out.includes(path)) out.push(path);
@@ -283,8 +283,17 @@ export function hasMainThreadAssistantText(transcriptPath) {
283
283
  if (!transcriptPath || !existsSync(transcriptPath)) return false;
284
284
  let raw;
285
285
  try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return false; }
286
- for (const line of raw.split('\n')) {
287
- if (!line.trim()) continue;
286
+ // Reverse-iterate so a turn that just produced text returns true on the
287
+ // FIRST line examined instead of walking the entire transcript. Common case
288
+ // (model wrote a paragraph → Stop fires) short-circuits in O(1) line parses;
289
+ // pathological case (no text anywhere) still walks all entries but that's
290
+ // the degenerate state we want false for anyway. v2.80 perf polish — the
291
+ // pre-v2.80 forward scan held the full transcript in memory and walked
292
+ // every line on the common case too.
293
+ const lines = raw.split('\n');
294
+ for (let i = lines.length - 1; i >= 0; i--) {
295
+ const line = lines[i];
296
+ if (!line || !line.trim()) continue;
288
297
  let entry;
289
298
  try { entry = JSON.parse(line); } catch { continue; }
290
299
  if (entry.type !== 'assistant' || !entry.message) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.79.0",
3
+ "version": "2.80.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
package/scripts/setup.sh CHANGED
@@ -83,13 +83,20 @@ mkdir -p "$DATA_DIR/runtime" 2>/dev/null || true
83
83
  mark_deps_broken() {
84
84
  local reason="$1"
85
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
86
+ # having to re-derive them. Delegate JSON serialization to node so embedded
87
+ # quotes / shell metachars in $ROOT or $reason can't produce an invalid file
88
+ # (bash `printf '"..%s.."'` cannot escape arbitrary strings safely; v2.79 fix).
89
+ MARK_REASON="$reason" MARK_ROOT="$ROOT" MARK_FLAG="$DEPS_FLAG" node -e '
90
+ const fs = require("fs");
91
+ const reason = process.env.MARK_REASON || "unknown";
92
+ const root = process.env.MARK_ROOT || "";
93
+ fs.writeFileSync(process.env.MARK_FLAG, JSON.stringify({
94
+ ts: new Date().toISOString(),
95
+ reason,
96
+ root,
97
+ repair: `cd ${JSON.stringify(root)} && npm install --omit=dev`,
98
+ }) + "\n");
99
+ ' 2>/dev/null || true
93
100
  }
94
101
 
95
102
  mark_deps_ok() {
@@ -120,18 +127,20 @@ else
120
127
  mark_deps_ok
121
128
  fi
122
129
 
123
- # 7. MCP cleanup: idempotently clean stale registrations from older installs.
124
- # This runs on every plugin SessionStart because old global entries may still
125
- # exist even after an earlier migration marker was written.
130
+ # 7. MCP cleanup: one-shot purge of stale global MCP registrations.
126
131
  # - Pre-2.10: direct installs left a global "mem" MCP alongside plugin MCP.
127
132
  # - Pre-2.78: plugin and global registrations used the generic name "mem";
128
133
  # v2.78 renamed to "mem-lite" — any stale global "mem" must be purged so
129
134
  # Claude Code doesn't surface duplicate (old "mem" + new "mem-lite") tool
130
135
  # prefixes side-by-side.
131
136
  # Root .mcp.json in the installed plugin cache is required for Claude Code to
132
- # register plugin MCP; only stale global/marketplace copies should be removed.
137
+ # register plugin MCP; only stale global/marketplace copies are removed.
138
+ # v2.79.1: marker file now actually gates entry (was touched but never read
139
+ # pre-v2.79.1 — extra node spawn + JSON parse on every SessionStart for a
140
+ # near-always no-op). Bump MCP_MIGRATION name to re-run cleanup in future
141
+ # versions; same shape as the .deps-broken self-heal pattern.
133
142
  MCP_MIGRATION="$DATA_DIR/runtime/.mcp-dedup-v2.78"
134
- if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
143
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$MCP_MIGRATION" ]]; then
135
144
  CLAUDE_JSON="$HOME/.claude.json" node -e '
136
145
  const fs = require("fs");
137
146
  let changed = false;
package/server.mjs CHANGED
@@ -2272,6 +2272,12 @@ process.on('unhandledRejection', (err) => { debugCatch(err, 'unhandledRejection'
2272
2272
  // server in one Claude Code session). Two records with close timestamps and
2273
2273
  // the same ppid is the smoking gun. Never throws — telemetry must not block
2274
2274
  // startup. Disable with MEM_DISABLE_SPAWN_LOG=1.
2275
+ //
2276
+ // File mode: no explicit chmod. Payload is `{ts, pid, ppid, argv1, version}` —
2277
+ // no secrets, no project content. Pre-v2.79.1 passed `{mode: 0o600}` to
2278
+ // appendFileSync but that only applies on file creation (umask-default after
2279
+ // the first append), so the "0600" claim was misleading honesty-wise. Honest
2280
+ // comment > misleading code.
2275
2281
  if (process.env.MEM_DISABLE_SPAWN_LOG !== '1') {
2276
2282
  try {
2277
2283
  if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true });
@@ -2282,7 +2288,7 @@ if (process.env.MEM_DISABLE_SPAWN_LOG !== '1') {
2282
2288
  argv1: process.argv[1] || '',
2283
2289
  version: PKG_VERSION,
2284
2290
  }) + '\n';
2285
- appendFileSync(join(RUNTIME_DIR, 'mcp-spawns.log'), line, { mode: 0o600 });
2291
+ appendFileSync(join(RUNTIME_DIR, 'mcp-spawns.log'), line);
2286
2292
  } catch { /* never block startup on telemetry failure */ }
2287
2293
  }
2288
2294