claude-mem-lite 3.95.0 → 3.95.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.95.0",
13
+ "version": "3.95.1",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.95.0",
3
+ "version": "3.95.1",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/README.md CHANGED
@@ -370,8 +370,12 @@ Slash commands `/adopt` and `/unadopt` wrap the same CLI.
370
370
  runtime-gated on sentinel presence, so projects without adoption get the
371
371
  full verbose output.
372
372
 
373
- See `docs/plans/2026-04-16-invited-memory-pattern.md` for the full design
374
- (including the reusable template other plugins can follow).
373
+ See [the invited-memory design][invited-memory] for the full design (including the
374
+ reusable template other plugins can follow). It is a development-time document and
375
+ is no longer in the repository at HEAD, so that link is pinned to `v3.95.0`, the
376
+ last release that carried it.
377
+
378
+ [invited-memory]: https://github.com/sdsrss/claude-mem-lite/blob/v3.95.0/docs/plans/2026-04-16-invited-memory-pattern.md
375
379
 
376
380
  ## Database Schema
377
381
 
package/hook-update.mjs CHANGED
@@ -1053,7 +1053,28 @@ function copyReleaseIntoStaging(sourceDir, stagingDir, manifest = { SOURCE_FILES
1053
1053
  // ── Cache hook residue clearing ────────────────────────────
1054
1054
  // Inline (does not import plugin-cache-guard.mjs) so hook-update.mjs keeps working
1055
1055
  // even if plugin-cache-guard.mjs is missing on disk in degraded installs.
1056
+
1057
+ // Mirror of plugin-cache-guard.hasInstallManagedHooks, inlined for the reason above.
1058
+ // Kept string-identical in its match rule (`.claude-mem-lite/` or `/claude-mem-lite/`
1059
+ // appearing in a serialized hooks block) so the two cannot disagree about whether
1060
+ // settings.json owns the hooks.
1061
+ function hasInstallManagedSettingsHooks() {
1062
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
1063
+ if (!existsSync(settingsPath)) return false;
1064
+ try {
1065
+ const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
1066
+ const serialized = JSON.stringify(s.hooks || {});
1067
+ return serialized.includes('.claude-mem-lite/') || serialized.includes('/claude-mem-lite/');
1068
+ } catch { return false; }
1069
+ }
1056
1070
  export function clearCacheHookResidue() {
1071
+ // Same precondition plugin-cache-guard.mjs documents and hook.mjs's self-heal
1072
+ // honours: this is a DEDUP against install.mjs-managed settings.json entries.
1073
+ // With no such entries the cache manifest is the ONLY hook registration, and
1074
+ // "clearing residue" unregisters all seven events — invisibly, because
1075
+ // status/doctor then see the shape of a healthy plugin-only install. Inlined
1076
+ // here for the same reason the rest of this function is (see header).
1077
+ if (!hasInstallManagedSettingsHooks()) return 0;
1057
1078
  const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'claude-mem-lite');
1058
1079
  if (!existsSync(cacheBase)) return 0;
1059
1080
  let cleared = 0;
package/install.mjs CHANGED
@@ -44,7 +44,7 @@ import { MARKETPLACE_KEY, PLUGIN_KEY, isPluginExplicitlyDisabled } from './lib/p
44
44
  const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
45
45
 
46
46
  import { RESOURCE_METADATA } from './install-metadata.mjs';
47
- import { scanPluginCacheHookPollution } from './plugin-cache-guard.mjs';
47
+ import { scanPluginCacheHookPollution, hasInstallManagedHooks, pluginCacheHookEvents } from './plugin-cache-guard.mjs';
48
48
  import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
49
49
  import { probeBetterSqlite3Binding, ensureBetterSqlite3Working, NATIVE_BINDING_REBUILD_CMD } from './lib/binding-probe.mjs';
50
50
  import { detectInstallShape, probeRuntimeRoots } from './lib/install-shape.mjs';
@@ -319,6 +319,40 @@ export function bumpJsonField(filePath, keyPath, newVal) {
319
319
  return { changed: true, prev };
320
320
  }
321
321
 
322
+ // CLAUDE.md's `- **Version**: x.y.z` line, patched to a new version.
323
+ //
324
+ // Replaces the version TOKEN, not the whole line. The line carries a trailing
325
+ // annotation ("— **this exact string is a release guard.**") and the previous
326
+ // whole-line form deleted it on the first release after that annotation was
327
+ // written. Every gate stayed green through the deletion — publish.yml greps the
328
+ // `^- **Version**: <semver>` prefix and install-e2e asserts the same substring,
329
+ // so neither can see a truncated tail. Pure + exported for the same reason
330
+ // bumpJsonField is: syncVersions gets one testable point of truth per file shape.
331
+ //
332
+ // @returns patched text, or null when the line is absent (caller warns + skips).
333
+ export function patchClaudeMdVersion(text, version) {
334
+ const versionLine = /^(- \*\*Version\*\*: )\d+\.\d+\.\d+(.*)$/m;
335
+ if (!versionLine.test(text)) return null;
336
+ return text.replace(versionLine, (_m, head, tail) => `${head}${version}${tail}`);
337
+ }
338
+
339
+ // Repair instruction for an unregistered hook manifest.
340
+ //
341
+ // The obvious advice — copy the marketplace clone over the cache copy — is a SILENT
342
+ // NO-OP in one real sequence (pre-ship review, finding 3): `install` empties the
343
+ // marketplace manifest too, so after `install` + `cleanup-hooks` BOTH files are
344
+ // `{"hooks":{}}` and the cp exits 0 having changed nothing, leaving the user staring
345
+ // at the same red line. Claude Code also seeds a NEW cache version from that same
346
+ // emptied clone. So check the source before prescribing it, and fall back to a
347
+ // reinstall — which re-clones the manifest from the repo — when it is empty too.
348
+ export function hookManifestRepairHint(cacheRoot, marketplaceRoot) {
349
+ const src = join(marketplaceRoot, 'hooks', 'hooks.json');
350
+ const dst = join(cacheRoot, 'hooks', 'hooks.json');
351
+ return pluginCacheHookEvents(marketplaceRoot).ok
352
+ ? `cp "${src}" "${dst}" && restart Claude Code`
353
+ : `no usable marketplace copy to restore from — reinstall the plugin (/plugin uninstall then /plugin install), then restart Claude Code`;
354
+ }
355
+
322
356
  // Doctor's final summary line. Pure function so the 4-way contract
323
357
  // (clean / warnings-only / issues / mixed) is unit-testable without spinning
324
358
  // up the full doctor pipeline. `issues` are ✗-level (action required);
@@ -597,7 +631,7 @@ if (pluginHandlesMcp) {
597
631
  }
598
632
  }
599
633
 
600
- function dedupePluginCacheAndHooks() {
634
+ function dedupePluginCacheAndHooks({ managedHooks } = {}) {
601
635
  // 3b. Deduplicate: if marketplace plugin also registers MCP + hooks,
602
636
  // clear them to prevent double execution. install.mjs hooks (in settings.json)
603
637
  // point to ~/.claude-mem-lite/ (latest code in dev mode via symlinks),
@@ -609,6 +643,32 @@ function dedupePluginCacheAndHooks() {
609
643
  const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY);
610
644
  const pluginHooksPath = join(pluginDir, 'hooks', 'hooks.json');
611
645
 
646
+ // Clearing is a DEDUP, and a dedup with only one registration left is a delete.
647
+ // Both clearers below empty a file Claude Code reads hooks from; that is correct
648
+ // only while settings.json ALSO registers them. On a plugin-only install (no
649
+ // install.mjs-managed entries) the cache manifest is the sole registration, so
650
+ // clearing it silently unregisters all seven events — and status/doctor then read
651
+ // "settings.json holds none" as the healthy plugin shape. plugin-cache-guard.mjs
652
+ // has documented this precondition since it was written and hook.mjs's self-heal
653
+ // honours it; these two sites did not.
654
+ //
655
+ // `managedHooks` comes from the caller rather than a bare hasInstallManagedHooks()
656
+ // call, and that is the whole point: install() runs configureHooks() first, so a
657
+ // self-read here is ALWAYS true and the guard would be decorative — the real
658
+ // protection would be the call ORDER, which nothing pins and a future reorder
659
+ // would silently revert (pre-ship review, finding 1). Passing the value makes the
660
+ // dependency data, not sequence. Explicit `false` is honoured; omitted → self-read,
661
+ // for any caller that has not just written settings.json.
662
+ const settingsOwnsHooks = managedHooks ?? hasInstallManagedHooks();
663
+
664
+ // Scope note (pre-ship review, finding 2): the gate covers the two hook-CLEARING
665
+ // blocks only. The launch.mjs / launch-preflight.mjs sync below it is not dedup —
666
+ // it is issue #15's dev-mode MCP routing fix — and an early return out of the whole
667
+ // function would silently stop shipping it to plugin-cache users.
668
+ if (!settingsOwnsHooks) {
669
+ log('Plugin cache: hooks left in place (plugin-only install — the cache manifest is the only registration)');
670
+ }
671
+
612
672
  if (existsSync(pluginDir)) {
613
673
  // NOTE: Do NOT clear marketplace .mcp.json — Claude Code copies from
614
674
  // marketplace clone → plugin cache on updates. Clearing it causes the
@@ -617,7 +677,7 @@ if (existsSync(pluginDir)) {
617
677
 
618
678
  // Clear plugin hooks to prevent double hook execution
619
679
  try {
620
- if (existsSync(pluginHooksPath)) {
680
+ if (settingsOwnsHooks && existsSync(pluginHooksPath)) {
621
681
  const pluginHooks = JSON.parse(readFileSync(pluginHooksPath, 'utf8'));
622
682
  if (pluginHooks.hooks && Object.keys(pluginHooks.hooks).length > 0) {
623
683
  // Atomic (audit 2026-09-02 P1-10): a torn hooks.json is not a fail-open marker —
@@ -659,7 +719,7 @@ if (existsSync(pluginDir)) {
659
719
 
660
720
  // Clear cached hooks.json (runtime reads here, not marketplace source)
661
721
  const cachedHooksPath = join(verDir, 'hooks', 'hooks.json');
662
- if (existsSync(cachedHooksPath)) {
722
+ if (settingsOwnsHooks && existsSync(cachedHooksPath)) {
663
723
  try {
664
724
  const h = JSON.parse(readFileSync(cachedHooksPath, 'utf8'));
665
725
  if (h.hooks && Object.keys(h.hooks).length > 0) {
@@ -862,6 +922,11 @@ writeSettings(settings);
862
922
  // kept saying five after the map changed, which is how a missing registration reads as
863
923
  // a successful one.
864
924
  ok(`Hooks configured (${Object.keys(hookConfigs).join(', ')})`);
925
+ // Returned so dedupePluginCacheAndHooks gates on a VALUE this function produced
926
+ // rather than re-reading settings.json — see the `managedHooks` note there. This
927
+ // function writes all seven events unconditionally, so the answer is always true;
928
+ // returning it keeps that fact in the caller's dataflow instead of in call order.
929
+ return true;
865
930
  }
866
931
 
867
932
  function backupLegacyClaudeMemData() {
@@ -1231,8 +1296,14 @@ async function install() {
1231
1296
  await installDependencies(IS_DEV);
1232
1297
  createCliSymlink();
1233
1298
  registerMcpServer();
1234
- dedupePluginCacheAndHooks();
1235
- configureHooks();
1299
+ // configureHooks BEFORE dedupe, and its result feeds the dedup gate: dedupe now
1300
+ // refuses to clear a hooks manifest unless install.mjs-managed hooks exist in
1301
+ // settings.json, and on a first install those entries do not exist until
1302
+ // configureHooks writes them. Passing the value (rather than letting dedupe
1303
+ // re-read settings.json) is what keeps a future reorder from silently turning the
1304
+ // dedup off — the dependency is data, not sequence.
1305
+ const managedHooks = configureHooks();
1306
+ dedupePluginCacheAndHooks({ managedHooks });
1236
1307
  backupLegacyClaudeMemData();
1237
1308
  await installPreinstalledResources();
1238
1309
  verifyDatabase();
@@ -1452,7 +1523,15 @@ async function status() {
1452
1523
  } else if (pluginDisabled) {
1453
1524
  push('ok', 'hooks', 'Hooks: not configured', { configured: false });
1454
1525
  } else if (pluginProvides) {
1455
- push('ok', 'hooks', `Hooks: provided by the plugin manifest (v${shape.activePluginVersion.version} hooks/hooks.json) settings.json correctly holds none`, { configured: false, via: 'plugin' });
1526
+ // Open the manifest being credited. Trusting `settings.json holds none` alone
1527
+ // reported all-green over an emptied cache manifest — zero hooks registered.
1528
+ const manifest = pluginCacheHookEvents(shape.activePluginVersion.root);
1529
+ if (manifest.ok) {
1530
+ push('ok', 'hooks', `Hooks: provided by the plugin manifest (v${shape.activePluginVersion.version} hooks/hooks.json, ${manifest.events.length} events) — settings.json correctly holds none`, { configured: false, via: 'plugin', events: manifest.events });
1531
+ } else {
1532
+ const repair = hookManifestRepairHint(shape.activePluginVersion.root, join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY));
1533
+ push('fail', 'hooks', `Hooks: plugin manifest v${shape.activePluginVersion.version} registers NO hooks (${manifest.reason}) and settings.json holds none — every hook is unregistered. Repair: ${repair}`, { configured: false, via: 'plugin', events: [], manifest_reason: manifest.reason });
1534
+ }
1456
1535
  } else {
1457
1536
  push('fail', 'hooks', 'Hooks: not configured', { configured: false });
1458
1537
  }
@@ -1688,8 +1767,17 @@ async function doctor() {
1688
1767
  } else if (shape.activePluginVersion) {
1689
1768
  // Plugin-only: hooks come from the cache's hooks/hooks.json, and an EMPTY
1690
1769
  // settings.json hooks block is the correct state — warning about it told a
1691
- // correctly-installed user their hooks were missing.
1692
- ok(`Plugin lifecycle: hooks served by the plugin manifest (v${shape.activePluginVersion.version}); settings.json correctly holds none`);
1770
+ // correctly-installed user their hooks were missing. But "correct state" is
1771
+ // only half the question: read the manifest too, or an emptied one passes as
1772
+ // the healthy shape (same false green as status).
1773
+ const manifest = pluginCacheHookEvents(shape.activePluginVersion.root);
1774
+ if (manifest.ok) {
1775
+ ok(`Plugin lifecycle: hooks served by the plugin manifest (v${shape.activePluginVersion.version}, ${manifest.events.length} events); settings.json correctly holds none`);
1776
+ } else {
1777
+ fail(`Plugin lifecycle: plugin manifest v${shape.activePluginVersion.version} registers NO hooks (${manifest.reason}) and settings.json holds none — every hook is unregistered`);
1778
+ log(` Repair: ${hookManifestRepairHint(shape.activePluginVersion.root, join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY))}`);
1779
+ issues++;
1780
+ }
1693
1781
  } else {
1694
1782
  dwarn('Plugin lifecycle: hooks not configured');
1695
1783
  }
@@ -2452,9 +2540,8 @@ function syncVersions() {
2452
2540
  const claudeMdPath = join(PROJECT_DIR, 'CLAUDE.md');
2453
2541
  if (existsSync(claudeMdPath)) {
2454
2542
  const orig = readFileSync(claudeMdPath, 'utf8');
2455
- const versionLine = /^- \*\*Version\*\*: .+$/m;
2456
- if (versionLine.test(orig)) {
2457
- const patched = orig.replace(versionLine, `- **Version**: ${version}`);
2543
+ const patched = patchClaudeMdVersion(orig, version);
2544
+ if (patched !== null) {
2458
2545
  if (patched !== orig) {
2459
2546
  writeFileSync(claudeMdPath, patched);
2460
2547
  ok(`CLAUDE.md: → ${version}`);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.95.0",
3
+ "version": "3.95.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.95.0",
9
+ "version": "3.95.1",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.30.0",
12
12
  "better-sqlite3": "^12.11.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.95.0",
3
+ "version": "3.95.1",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -64,6 +64,36 @@ export function clearPluginCacheHooks(opts) {
64
64
  return cleared.sort();
65
65
  }
66
66
 
67
+ /**
68
+ * What a cache version's hooks/hooks.json ACTUALLY registers.
69
+ *
70
+ * The counterpart to hasInstallManagedHooks: on a plugin-only install this file
71
+ * is the sole hook registration, so an EMPTY one means every hook is dead. Both
72
+ * status and doctor used to credit "the plugin manifest serves the hooks" from
73
+ * `settings.json holds none` + `an active plugin version exists` alone, never
74
+ * opening the manifest — and an emptied manifest looks exactly like a healthy
75
+ * npm-shape install from those two facts. That false green is what let a cleared
76
+ * cache run for a full session reporting all-green with zero hooks firing.
77
+ *
78
+ * @param {string} root Cache version dir (shape.activePluginVersion.root)
79
+ * @returns {{ok: boolean, events: string[], reason: string|null}}
80
+ * ok=false with reason 'no-manifest' | 'unreadable' | 'empty'
81
+ */
82
+ export function pluginCacheHookEvents(root) {
83
+ const p = join(root, 'hooks', 'hooks.json');
84
+ if (!existsSync(p)) return { ok: false, events: [], reason: 'no-manifest' };
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(readFileSync(p, 'utf8'));
88
+ } catch {
89
+ return { ok: false, events: [], reason: 'unreadable' };
90
+ }
91
+ const events = Object.keys(parsed?.hooks || {});
92
+ return events.length > 0
93
+ ? { ok: true, events, reason: null }
94
+ : { ok: false, events: [], reason: 'empty' };
95
+ }
96
+
67
97
  export function hasInstallManagedHooks(opts) {
68
98
  const home = opts?.home || homedir();
69
99
  const plugin = opts?.plugin || DEFAULT_PLUGIN;
package/scripts/setup.sh CHANGED
@@ -26,7 +26,13 @@ fi
26
26
  log_ok() { echo -e "${GREEN}✓${NC} $*" >&2; }
27
27
  log_info() { echo -e "${BLUE}ℹ${NC} $*" >&2; }
28
28
  log_warn() { echo -e "${YELLOW}⚠${NC} $*" >&2; }
29
- # shellcheck disable=SC2317 # kept for API symmetry with log_ok/log_info/log_warn
29
+ # shellcheck disable=SC2317,SC2329 # kept for API symmetry with log_ok/log_info/log_warn
30
+ # Both codes on purpose: shellcheck 0.10.0 split "this function is never invoked" out of
31
+ # SC2317 (unreachable command) into its own SC2329. The lone SC2317 stopped matching, so
32
+ # 0.11.0 flags this line and `npx eslint`-style local runs exit 1 while CI stays green —
33
+ # the ubuntu-latest runner still ships a pre-0.10 shellcheck. That is a version skew, not
34
+ # a disagreement about the code: this job gates on exit 0, so it turns red on its own the
35
+ # day GitHub bumps the runner image. Keep SC2317 for anyone on an older shellcheck.
30
36
  log_err() { echo -e "${RED}✗${NC} $*" >&2; }
31
37
 
32
38
  # 1. Migrate unhidden dir (~/claude-mem-lite/ → ~/.claude-mem-lite/)