claude-mem-lite 2.53.2 → 2.55.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.53.2",
13
+ "version": "2.55.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.53.2",
3
+ "version": "2.55.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/hook-llm.mjs CHANGED
@@ -677,9 +677,13 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
677
677
  files: episode.files,
678
678
  filesRead: episode.filesRead || [],
679
679
  // v2.33.1: when lesson is low-signal, don't trust Haiku's importance
680
- // inflation for noise-prone types. rule-based floor still applies so
681
- // error-in-test (→3) / config-change (→2) keep their floor.
682
- importance: isLessonLowSignal && (parsed.type === 'change' || parsed.type === 'discovery')
680
+ // inflation. v2.54.0: extended from {change, discovery} to all types
681
+ // except `decision` after audit (2026-04-30) showed bugfix lesson
682
+ // coverage 11.2% / refactor hit-rate 18.1% Haiku marks bugfix/refactor
683
+ // imp=2-3 even when lesson is null after retry. Keep `decision` exempt:
684
+ // it's rare (39 obs / 94.9% hit-rate) and the retry path already gave
685
+ // it a second chance; a no-lesson decision is still a worthwhile signal.
686
+ importance: isLessonLowSignal && parsed.type !== 'decision'
683
687
  ? Math.min(ruleImportance, 1)
684
688
  : Math.max(ruleImportance, clampImportance(parsed.importance)),
685
689
  lessonLearned,
package/hook-optimize.mjs CHANGED
@@ -709,7 +709,14 @@ export async function handleLLMOptimize() {
709
709
  }
710
710
 
711
711
  try {
712
- const results = await optimizeRun(db);
712
+ // v2.54.0: auto-maintain default scope is 'wide'. Narrow scope (the prior
713
+ // default) only matches fully-degraded rows (no concepts AND no facts AND
714
+ // no lesson AND no aliases) — production diagnostic 2026-04-30 found only
715
+ // 56 obs ever optimized after months of daily auto-maintain runs. Wide
716
+ // targets bugfix/refactor/feature/decision rows with substantive narrative
717
+ // but missing lesson_learned, which is exactly the audit's 11.2% coverage
718
+ // gap. CLI `mem optimize` keeps narrow as default for explicit invocations.
719
+ const results = await optimizeRun(db, { reenrichScope: 'wide' });
713
720
  const parts = [];
714
721
  if (results.reenrich?.processed) parts.push(`re-enriched: ${results.reenrich.processed}`);
715
722
  if (results.normalize?.processed) parts.push(`normalized: ${results.normalize.processed}`);
package/hook-update.mjs CHANGED
@@ -3,12 +3,12 @@
3
3
  // Skips in dev mode (symlinked installs). Silent on network failure.
4
4
 
5
5
  import { execSync, execFileSync } from 'node:child_process';
6
- import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync } from 'node:fs';
6
+ import { readFileSync, writeFileSync, copyFileSync, cpSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
8
  import { tmpdir, homedir } from 'node:os';
9
9
  import { DB_DIR } from './schema.mjs';
10
10
  import { debugCatch, debugLog } from './utils.mjs';
11
- import { SOURCE_FILES } from './source-files.mjs';
11
+ import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
12
12
 
13
13
  // ── Configuration ──────────────────────────────────────────
14
14
  const GITHUB_REPO = 'sdsrss/claude-mem-lite';
@@ -328,16 +328,30 @@ function copyReleaseIntoStaging(sourceDir, stagingDir) {
328
328
  copied++;
329
329
  }
330
330
 
331
- for (const dirName of ['scripts', 'registry']) {
332
- const srcDir = join(sourceDir, dirName);
333
- const destDir = join(stagingDir, dirName);
334
- if (!existsSync(srcDir)) continue;
335
- mkdirSync(destDir, { recursive: true });
336
- for (const entry of readdirSync(srcDir)) {
337
- copyFileSync(join(srcDir, entry), join(destDir, entry));
331
+ // scripts/ is curated to HOOK_SCRIPT_FILES — settings.json hook commands
332
+ // resolve only to these 5 files, and plugin mode does not consume this
333
+ // directory at all. Pre-v2.55 used cpSync({recursive:true}) which silently
334
+ // shipped dev-only files (mock-claude.mjs, extract-repos.mjs, p0-forward-probe.mjs…)
335
+ // from the GitHub Releases tarball into every user's data dir.
336
+ const stagingScripts = join(stagingDir, 'scripts');
337
+ const sourceScripts = join(sourceDir, 'scripts');
338
+ if (existsSync(sourceScripts)) {
339
+ mkdirSync(stagingScripts, { recursive: true });
340
+ for (const name of HOOK_SCRIPT_FILES) {
341
+ const src = join(sourceScripts, name);
342
+ if (existsSync(src)) copyFileSync(src, join(stagingScripts, name));
338
343
  }
339
344
  }
340
345
 
346
+ // registry/ stays recursive — preinstalled.json is the only current entry
347
+ // but the directory is consumed wholesale by the registry indexer and may
348
+ // grow subtrees. Pre-v2.55 readdirSync+copyFileSync would EISDIR-throw on
349
+ // any subdir and silently roll back the entire update.
350
+ const sourceRegistry = join(sourceDir, 'registry');
351
+ if (existsSync(sourceRegistry)) {
352
+ cpSync(sourceRegistry, join(stagingDir, 'registry'), { recursive: true });
353
+ }
354
+
341
355
  const stagedScripts = join(stagingDir, 'scripts');
342
356
  if (existsSync(stagedScripts)) {
343
357
  for (const sf of readdirSync(stagedScripts).filter(n => n.endsWith('.sh'))) {
package/install.mjs CHANGED
@@ -29,23 +29,13 @@ import { createRequire } from 'module';
29
29
 
30
30
  import { RESOURCE_METADATA } from './install-metadata.mjs';
31
31
  import { scanPluginCacheHookPollution } from './plugin-cache-guard.mjs';
32
- import { SOURCE_FILES } from './source-files.mjs';
32
+ import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
33
33
 
34
- /**
35
- * Hook scripts that non-dev install must copy into ~/.claude-mem-lite/scripts/
36
- * to keep settings.json hook commands resolvable. Single source of truth so
37
- * adding a new PreToolUse/PostToolUse hook script can't drift from the install
38
- * copy block (which previously hand-listed only 3 of these and silently
39
- * dropped pre-tool-recall.js + pre-skill-bridge.js — every fresh install left
40
- * settings.json pointing at non-existent files).
41
- */
42
- export const HOOK_SCRIPT_FILES = [
43
- 'post-tool-use.sh',
44
- 'user-prompt-search.js',
45
- 'prompt-search-utils.mjs',
46
- 'pre-tool-recall.js',
47
- 'pre-skill-bridge.js',
48
- ];
34
+ // Re-export for backward compatibility — tests/install-hook-scripts.test.mjs
35
+ // and any external consumers still import HOOK_SCRIPT_FILES from install.mjs.
36
+ // The constant itself moved to source-files.mjs in v2.55 so hook-update.mjs
37
+ // can share it without a static cycle.
38
+ export { HOOK_SCRIPT_FILES };
49
39
 
50
40
  export function copyHookScripts(srcDir, destDir) {
51
41
  for (const name of HOOK_SCRIPT_FILES) {
@@ -349,12 +339,10 @@ async function install() {
349
339
  if (existsSync(join(PROJECT_DIR, 'registry'))) {
350
340
  symlinkSync(join(PROJECT_DIR, 'registry'), regLink);
351
341
  }
352
- // Symlink commands/ directory
353
- const cmdLink = join(DATA_DIR, 'commands');
354
- if (existsSync(cmdLink)) try { rmSync(cmdLink, { recursive: true, force: true }); } catch {}
355
- if (existsSync(join(PROJECT_DIR, 'commands'))) {
356
- symlinkSync(join(PROJECT_DIR, 'commands'), cmdLink);
357
- }
342
+ // commands/ is intentionally NOT linked: Claude Code reads slash commands
343
+ // from the plugin cache (~/.claude/plugins/cache/<mp>/<plugin>/<ver>/commands/)
344
+ // or user-level ~/.claude/commands/, never from ~/.claude-mem-lite/commands/.
345
+ // Pre-v2.55 maintained a symlink/copy here that had no consumers.
358
346
  ok('Symlinks created in ~/.claude-mem-lite/ → dev dir');
359
347
  } else {
360
348
  log('Installing to ~/.claude-mem-lite/...');
@@ -375,15 +363,7 @@ async function install() {
375
363
  copyHookScripts(join(PROJECT_DIR, 'scripts'), scriptsDir);
376
364
  // Ensure bash script is executable
377
365
  try { execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' }); } catch {}
378
- // Copy commands directory
379
- const commandsDir = join(DATA_DIR, 'commands');
380
- if (!existsSync(commandsDir)) mkdirSync(commandsDir, { recursive: true });
381
- const commandsSrc = join(PROJECT_DIR, 'commands');
382
- if (existsSync(commandsSrc)) {
383
- for (const f of readdirSync(commandsSrc).filter(f => f.endsWith('.md'))) {
384
- copyFileSync(join(commandsSrc, f), join(commandsDir, f));
385
- }
386
- }
366
+ // commands/ is intentionally NOT copied — see dev-mode branch above.
387
367
  // Copy registry manifest
388
368
  const registryDir = join(DATA_DIR, 'registry');
389
369
  if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
@@ -1614,9 +1594,10 @@ function syncVersions() {
1614
1594
  const marketJson = JSON.parse(readFileSync(marketJsonPath, 'utf8'));
1615
1595
  const plugin = marketJson.plugins?.[0];
1616
1596
  if (plugin && plugin.version !== version) {
1597
+ const prev = plugin.version;
1617
1598
  plugin.version = version;
1618
1599
  writeFileSync(marketJsonPath, JSON.stringify(marketJson, null, 2) + '\n');
1619
- ok(`marketplace.json: ${plugin.version} → ${version}`);
1600
+ ok(`marketplace.json: ${prev} → ${version}`);
1620
1601
  } else if (plugin) {
1621
1602
  ok(`marketplace.json: already ${version}`);
1622
1603
  }
@@ -1624,6 +1605,27 @@ function syncVersions() {
1624
1605
  warn('marketplace.json not found');
1625
1606
  }
1626
1607
 
1608
+ // Sync CLAUDE.md `**Version**: x.y.z` line — install-e2e asserts this
1609
+ // matches package.json so omitting it here would break CI on every release.
1610
+ const claudeMdPath = join(PROJECT_DIR, 'CLAUDE.md');
1611
+ if (existsSync(claudeMdPath)) {
1612
+ const orig = readFileSync(claudeMdPath, 'utf8');
1613
+ const versionLine = /^- \*\*Version\*\*: .+$/m;
1614
+ if (versionLine.test(orig)) {
1615
+ const patched = orig.replace(versionLine, `- **Version**: ${version}`);
1616
+ if (patched !== orig) {
1617
+ writeFileSync(claudeMdPath, patched);
1618
+ ok(`CLAUDE.md: → ${version}`);
1619
+ } else {
1620
+ ok(`CLAUDE.md: already ${version}`);
1621
+ }
1622
+ } else {
1623
+ warn('CLAUDE.md: `**Version**:` line not found — skipped');
1624
+ }
1625
+ } else {
1626
+ warn('CLAUDE.md not found');
1627
+ }
1628
+
1627
1629
  console.log('');
1628
1630
  }
1629
1631
 
@@ -155,19 +155,26 @@ export function isNoiseObservation(obs, env = process.env) {
155
155
  const lesson = obs.lessonLearned ?? obs.lesson_learned;
156
156
  if (lesson && String(lesson).trim() && String(lesson).trim().toLowerCase() !== 'none') return false;
157
157
 
158
- if ((obs.importance ?? 1) >= 2) return false;
159
-
160
- if (Array.isArray(obs.facts) &&
161
- obs.facts.filter(f => typeof f === 'string' && f.trim().length > 0).length >= 1) {
162
- return false;
163
- }
158
+ const hasFacts = Array.isArray(obs.facts) &&
159
+ obs.facts.filter(f => typeof f === 'string' && f.trim().length > 0).length >= 1;
160
+ if (hasFacts) return false;
164
161
 
165
162
  const narrative = (obs.narrative || '').trim();
166
- if (narrative.length >= 40 &&
167
- !/^Error[: ]/i.test(narrative) &&
168
- !_isLikelyToolOutputPassthrough(narrative)) {
169
- return false;
170
- }
163
+ const isPassthrough = _isLikelyToolOutputPassthrough(narrative);
164
+ const isStderrShape = /^Error[: ]/i.test(narrative);
165
+
166
+ // v2.54.0: raw tool-output passthrough is always noise regardless of importance.
167
+ // Rule-based importance (computeRuleImportance) can hit 2-3 from filename
168
+ // heuristics (test/schema/migration) even when narrative is just
169
+ // "cmd → ERROR: stderr" — 30d audit found 64 'Error: X' titles surviving via
170
+ // imp=2 escape with raw stderr narratives. Per #8152 paired-gate model: this
171
+ // is the drop counterpart to capNoiseImportance's demote — both must check
172
+ // the same passthrough signal.
173
+ if (isPassthrough || isStderrShape) return true;
174
+
175
+ if ((obs.importance ?? 1) >= 2) return false;
176
+
177
+ if (narrative.length >= 40) return false;
171
178
 
172
179
  return true;
173
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.53.2",
3
+ "version": "2.55.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
package/scripts/setup.sh CHANGED
@@ -136,5 +136,53 @@ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
136
136
  fi
137
137
  fi
138
138
 
139
+ # 9. Residue detection (plugin mode only): warn once if legacy direct-install
140
+ # hooks remain in ~/.claude/settings.json. A user who installed via global
141
+ # `claude-mem-lite install` and later switched to the marketplace plugin
142
+ # will run every hook twice (direct settings.json hooks AND plugin hooks)
143
+ # until they run `claude-mem-lite uninstall` to clear the settings.json
144
+ # entries. /plugin uninstall does not touch settings.json.
145
+ RESIDUE_MARKER="$DATA_DIR/runtime/.residue-warned-v2.55"
146
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$RESIDUE_MARKER" ]]; then
147
+ SETTINGS="$HOME/.claude/settings.json"
148
+ if [[ -f "$SETTINGS" ]]; then
149
+ SETTINGS_PATH="$SETTINGS" node -e '
150
+ const fs = require("fs");
151
+ try {
152
+ const raw = fs.readFileSync(process.env.SETTINGS_PATH, "utf8");
153
+ const data = JSON.parse(raw);
154
+ const hooks = data.hooks || {};
155
+ const events = Object.keys(hooks);
156
+ const found = [];
157
+ for (const ev of events) {
158
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
159
+ for (const entry of list) {
160
+ const inner = Array.isArray(entry?.hooks) ? entry.hooks : [];
161
+ for (const h of inner) {
162
+ const cmd = String(h?.command || "");
163
+ if (cmd.includes(".claude-mem-lite/") || cmd.includes("claude-mem-lite/scripts") || cmd.includes("claude-mem-lite/hook.mjs")) {
164
+ found.push(ev);
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ if (found.length) {
171
+ process.stderr.write("\n");
172
+ process.stderr.write("\x1b[33m⚠\x1b[0m Legacy direct-install hooks detected in " + process.env.SETTINGS_PATH + "\n");
173
+ process.stderr.write(" Events with stale entries: " + [...new Set(found)].join(", ") + "\n");
174
+ process.stderr.write(" These will fire alongside plugin hooks (each tool call runs twice).\n");
175
+ process.stderr.write(" Fix: run \x1b[1mclaude-mem-lite uninstall\x1b[0m to clear settings.json,\n");
176
+ process.stderr.write(" then keep using the plugin install. (One-time warning.)\n\n");
177
+ process.exit(2);
178
+ }
179
+ } catch {}
180
+ ' || true
181
+ fi
182
+ # Mark the warning as shown regardless of result — silence is fine if no
183
+ # residue, and the warning above is one-shot per data-dir.
184
+ touch "$RESIDUE_MARKER"
185
+ fi
186
+
139
187
  log_ok "claude-mem-lite ready"
140
188
  exit 0
package/source-files.mjs CHANGED
@@ -53,3 +53,23 @@ export const SOURCE_FILES = [
53
53
  'adopt-content.mjs',
54
54
  'adopt-cli.mjs',
55
55
  ];
56
+
57
+ /**
58
+ * Hook scripts that direct-install (non-plugin) mode must materialize under
59
+ * ~/.claude-mem-lite/scripts/ — settings.json hook commands resolve to these
60
+ * absolute paths. Plugin mode does not consume this directory (it runs scripts
61
+ * from ${CLAUDE_PLUGIN_ROOT} instead).
62
+ *
63
+ * Single source of truth for both install.mjs (initial install) and
64
+ * hook-update.mjs (auto-update): pre-v2.55 hook-update copied the entire
65
+ * scripts/ tree from the GitHub Releases tarball, which silently shipped
66
+ * dev-only files (mock-claude.mjs, extract-repos.mjs, p0-forward-probe.mjs…)
67
+ * to every user's data dir on the first auto-update.
68
+ */
69
+ export const HOOK_SCRIPT_FILES = [
70
+ 'post-tool-use.sh',
71
+ 'user-prompt-search.js',
72
+ 'prompt-search-utils.mjs',
73
+ 'pre-tool-recall.js',
74
+ 'pre-skill-bridge.js',
75
+ ];