claude-mem-lite 4.0.0 → 4.0.2

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": "4.0.0",
13
+ "version": "4.0.2",
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": "4.0.0",
3
+ "version": "4.0.2",
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
@@ -145,7 +145,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
145
145
 
146
146
  ## Requirements
147
147
 
148
- - **Node.js** >= 20
148
+ - **Node.js** >= 22
149
149
  - **Claude Code** CLI installed and configured (`claude` command available)
150
150
  - **SQLite3** support (provided by `better-sqlite3`, compiled on install)
151
151
  - **Platform**: Linux or macOS (see [Platform Support](#platform-support))
package/hook-optimize.mjs CHANGED
@@ -169,7 +169,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
169
169
  const stmt = db.prepare(`
170
170
  SELECT id, title, narrative, type, subtitle, importance, project
171
171
  FROM observations
172
- WHERE COALESCE(compressed_into, 0) = 0
172
+ WHERE ${liveObsFilterSql('')}
173
173
  AND (concepts IS NULL OR concepts = '')
174
174
  AND (facts IS NULL OR facts = '')
175
175
  AND lesson_learned IS NULL
@@ -468,7 +468,7 @@ export function extractUniqueConcepts(db, limit = 500, { project } = {}) {
468
468
  const projectClause = project ? 'AND project = ?' : '';
469
469
  const stmt = db.prepare(`
470
470
  SELECT concepts FROM observations
471
- WHERE COALESCE(compressed_into, 0) = 0
471
+ WHERE ${liveObsFilterSql('')}
472
472
  AND concepts IS NOT NULL AND concepts != ''
473
473
  ${projectClause}
474
474
  ORDER BY created_at_epoch DESC
@@ -536,7 +536,7 @@ export function applyNormalization(db, groups, { project = null } = {}) {
536
536
  .prepare(
537
537
  `
538
538
  SELECT id, title, narrative, concepts, search_aliases, lesson_learned FROM observations
539
- WHERE COALESCE(compressed_into, 0) = 0
539
+ WHERE ${liveObsFilterSql('')}
540
540
  AND concepts IS NOT NULL AND concepts != ''
541
541
  AND (? IS NULL OR project = ?)
542
542
  `,
package/install.mjs CHANGED
@@ -68,7 +68,7 @@ import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
68
68
  import {
69
69
  probeBetterSqlite3Binding,
70
70
  ensureBetterSqlite3Working,
71
- NATIVE_BINDING_REBUILD_CMD,
71
+ nativeBindingRepairHint,
72
72
  } from './lib/binding-probe.mjs';
73
73
  import { detectInstallShape, probeRuntimeRoots } from './lib/install-shape.mjs';
74
74
  import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
@@ -404,6 +404,57 @@ export function patchClaudeMdVersion(text, version) {
404
404
  // at the same red line. Claude Code also seeds a NEW cache version from that same
405
405
  // emptied clone. So check the source before prescribing it, and fall back to a
406
406
  // reinstall — which re-clones the manifest from the repo — when it is empty too.
407
+ /**
408
+ * Clear whatever occupies `p` — file, directory, or symlink INCLUDING a dangling one —
409
+ * before a symlink is written over it. Returns true when something was removed.
410
+ *
411
+ * A20260906-R8-P2-3: every call site used to gate on `existsSync(p)`, which FOLLOWS the
412
+ * link, so a dangling symlink reads as absent. Two consequences, and the second is the
413
+ * one that bites: `uninstall` leaves a dead `claude-mem-lite` on PATH, and
414
+ * `createCliSymlink` skips the removal, `symlinkSync` throws EEXIST, the catch falls back
415
+ * to an unwritable /usr/local/bin, and the user is told "CLI symlink failed — run
416
+ * manually". Re-running `install` — the documented repair — cannot repair it. The same
417
+ * file already knew the idiom: isDevInstall() pairs existsSync with lstatSync.
418
+ *
419
+ * rmSync rather than unlinkSync because the dev-mode sites link DIRECTORIES; verified it
420
+ * removes the link and leaves the target intact (a link to a populated dir, target still
421
+ * readable afterwards) — following it would delete the developer's own scripts/.
422
+ *
423
+ * @param {string} p
424
+ * @returns {boolean}
425
+ */
426
+ export function clearLinkPath(p) {
427
+ try {
428
+ lstatSync(p);
429
+ } catch {
430
+ return false; // genuinely absent — nothing to clear
431
+ }
432
+ try {
433
+ rmSync(p, { recursive: true, force: true });
434
+ return true;
435
+ } catch {
436
+ return false; // permissions; callers fall back or report their own failure
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Minimum supported Node MAJOR, parsed out of a `package.json#engines.node` range.
442
+ *
443
+ * A20260906-R8-P2-1: doctor carried its own `>= 18` literal, so v4.0.0 raised the real
444
+ * floor to 22 (npm refuses to install below it, and better-sqlite3 13 ships no prebuild
445
+ * for those Nodes) while doctor kept printing `✓ Node.js: v20.x` — greenlighting the
446
+ * runtime that WAS the fault, in the one tool a broken user is told to run. One source,
447
+ * so the two cannot drift again; the fallback only covers an unreadable manifest.
448
+ *
449
+ * @param {unknown} enginesNode e.g. '>=22' or '^22.12.0 || ^24.0.0 || >=26.0.0'
450
+ * @param {number} [fallback]
451
+ * @returns {number}
452
+ */
453
+ export function requiredNodeMajor(enginesNode, fallback = 22) {
454
+ const m = /(\d+)/.exec(String(enginesNode ?? ''));
455
+ return m ? Number(m[1]) : fallback;
456
+ }
457
+
407
458
  export function hookManifestRepairHint(cacheRoot, marketplaceRoot) {
408
459
  const src = join(marketplaceRoot, 'hooks', 'hooks.json');
409
460
  const dst = join(cacheRoot, 'hooks', 'hooks.json');
@@ -513,34 +564,22 @@ function installSourceFiles(IS_DEV) {
513
564
  // Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
514
565
  const linkParent = dirname(link);
515
566
  if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
516
- // Remove existing file/symlink before creating
517
- if (existsSync(link))
518
- try {
519
- unlinkSync(link);
520
- } catch {}
567
+ // Remove existing file/symlink (including a dangling one) before creating.
568
+ clearLinkPath(link);
521
569
  symlinkSync(target, link);
522
570
  }
523
571
  }
524
572
  // Symlink scripts/ directory
525
573
  const scriptsLink = join(DATA_DIR, 'scripts');
526
- if (existsSync(scriptsLink))
527
- try {
528
- rmSync(scriptsLink, { recursive: true, force: true });
529
- } catch {}
574
+ clearLinkPath(scriptsLink);
530
575
  symlinkSync(join(PROJECT_DIR, 'scripts'), scriptsLink);
531
576
  // Symlink node_modules/
532
577
  const nmLink = join(DATA_DIR, 'node_modules');
533
- if (existsSync(nmLink))
534
- try {
535
- rmSync(nmLink, { recursive: true, force: true });
536
- } catch {}
578
+ clearLinkPath(nmLink);
537
579
  symlinkSync(join(PROJECT_DIR, 'node_modules'), nmLink);
538
580
  // Symlink registry/ directory
539
581
  const regLink = join(DATA_DIR, 'registry');
540
- if (existsSync(regLink))
541
- try {
542
- rmSync(regLink, { recursive: true, force: true });
543
- } catch {}
582
+ clearLinkPath(regLink);
544
583
  if (existsSync(join(PROJECT_DIR, 'registry'))) {
545
584
  symlinkSync(join(PROJECT_DIR, 'registry'), regLink);
546
585
  }
@@ -620,9 +659,7 @@ async function installDependencies(IS_DEV) {
620
659
  ok(`better-sqlite3: ${verify.action}`);
621
660
  } else {
622
661
  fail(`better-sqlite3 binding unusable after rebuild: ${verify.error}`);
623
- log(
624
- 'Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --dangerously-allow-all-scripts',
625
- );
662
+ log(`Try manually: ${nativeBindingRepairHint(INSTALL_DIR)}`);
626
663
  process.exit(1);
627
664
  }
628
665
 
@@ -643,7 +680,7 @@ async function installDependencies(IS_DEV) {
643
680
  `better-sqlite3 unusable in the package this installer runs from (${PROJECT_DIR}): ${selfVerify.error}`,
644
681
  );
645
682
  log(
646
- ` The install itself is fine; the \`claude-mem-lite\` shell command will self-heal on first use, or run: cd ${PROJECT_DIR} && ${NATIVE_BINDING_REBUILD_CMD}`,
683
+ ` The install itself is fine; the \`claude-mem-lite\` shell command will self-heal on first use, or run: ${nativeBindingRepairHint(PROJECT_DIR)}`,
647
684
  );
648
685
  }
649
686
  }
@@ -662,14 +699,14 @@ function createCliSymlink() {
662
699
  const cliLink = join(localBin, 'claude-mem-lite');
663
700
  try {
664
701
  if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
665
- if (existsSync(cliLink)) unlinkSync(cliLink);
702
+ clearLinkPath(cliLink);
666
703
  symlinkSync(cliSource, cliLink);
667
704
  ok(`CLI: ${cliLink} → ${cliSource}`);
668
705
  } catch {
669
706
  // Fallback: try /usr/local/bin (may need sudo)
670
707
  try {
671
708
  const globalLink = '/usr/local/bin/claude-mem-lite';
672
- if (existsSync(globalLink)) unlinkSync(globalLink);
709
+ clearLinkPath(globalLink);
673
710
  symlinkSync(cliSource, globalLink);
674
711
  ok(`CLI: ${globalLink} → ${cliSource}`);
675
712
  } catch {
@@ -1545,8 +1582,7 @@ async function uninstall() {
1545
1582
  for (const binDir of [join(homedir(), '.local', 'bin'), '/usr/local/bin']) {
1546
1583
  const cliLink = join(binDir, 'claude-mem-lite');
1547
1584
  try {
1548
- if (existsSync(cliLink)) {
1549
- unlinkSync(cliLink);
1585
+ if (clearLinkPath(cliLink)) {
1550
1586
  ok(`CLI symlink removed: ${cliLink}`);
1551
1587
  }
1552
1588
  } catch {
@@ -1901,12 +1937,22 @@ async function doctor() {
1901
1937
  warn(msg);
1902
1938
  };
1903
1939
 
1904
- // Node version
1940
+ // Node version. The floor is read from package.json#engines rather than restated here —
1941
+ // see requiredNodeMajor. It is PRINTED on the ok line too, so the guard test has something
1942
+ // to compare against the manifest; a floor nobody can observe is a floor nobody notices
1943
+ // has gone stale, which is exactly how the `>= 18` literal outlived the v4.0.0 bump.
1905
1944
  const nodeVer = process.version;
1906
- if (parseInt(nodeVer.slice(1)) >= 18) {
1907
- ok(`Node.js: ${nodeVer}`);
1945
+ let enginesNode = null;
1946
+ try {
1947
+ enginesNode = JSON.parse(readFileSync(join(PROJECT_DIR, 'package.json'), 'utf8')).engines?.node;
1948
+ } catch {
1949
+ /* unreadable manifest → requiredNodeMajor's fallback */
1950
+ }
1951
+ const nodeFloor = requiredNodeMajor(enginesNode);
1952
+ if (parseInt(nodeVer.slice(1)) >= nodeFloor) {
1953
+ ok(`Node.js: ${nodeVer} (>=${nodeFloor} required)`);
1908
1954
  } else {
1909
- fail(`Node.js ${nodeVer} too old (need >=18)`);
1955
+ fail(`Node.js ${nodeVer} too old (need >=${nodeFloor})`);
1910
1956
  issues++;
1911
1957
  }
1912
1958
 
@@ -3094,7 +3140,7 @@ async function rebuildBinding() {
3094
3140
  ok(`better-sqlite3 binding ${verify.action} for Node ${process.version} — ${label} (${root})`);
3095
3141
  } else {
3096
3142
  fail(`better-sqlite3 binding still unusable in ${label}: ${verify.error}`);
3097
- log(`Try manually: cd ${root} && ${NATIVE_BINDING_REBUILD_CMD}`);
3143
+ log(`Try manually: ${nativeBindingRepairHint(root)}`);
3098
3144
  failed++;
3099
3145
  }
3100
3146
  }
@@ -11,8 +11,10 @@ import { createRequire } from 'node:module';
11
11
  import { join } from 'node:path';
12
12
 
13
13
  // npm >= 12 blocks lifecycle scripts by default, so a plain `npm rebuild` exits 0
14
- // WITHOUT compiling — see the rebuild() comment below. Single home for the one
15
- // command that actually works, so hints, docs and heal paths cannot drift apart.
14
+ // WITHOUT compiling — see the rebuild() comment below. Step 1 of the heal chain, and
15
+ // correct for a dependency that declares an install script. It is NOT sufficient for the
16
+ // better-sqlite3 the project actually ships — see the constant below, and never hand this
17
+ // one to a human on its own (nativeBindingRepairHint is what surfaces get).
16
18
  export const NATIVE_BINDING_REBUILD_CMD = 'npm rebuild better-sqlite3 --dangerously-allow-all-scripts';
17
19
 
18
20
  // Last-resort heal, added in v4.0.0 with better-sqlite3 13. THE npm-REBUILD PATH ABOVE
@@ -29,6 +31,33 @@ export const NATIVE_BINDING_REBUILD_CMD = 'npm rebuild better-sqlite3 --dangerou
29
31
  // nothing → this command produces build/Release/better_sqlite3.node and the DB opens.
30
32
  export const NATIVE_BINDING_SOURCE_BUILD_CMD = 'npm run --prefix node_modules/better-sqlite3 build-release';
31
33
 
34
+ /**
35
+ * The one-liner a HUMAN should be given to repair the binding under `root`.
36
+ *
37
+ * A20260906-R8-P1-1: every user-facing "Repair:" line printed NATIVE_BINDING_REBUILD_CMD
38
+ * alone — including doctor's, via install-shape. On better-sqlite3 13 that command prints
39
+ * "rebuilt dependencies successfully", exits 0, and compiles nothing (re-measured
40
+ * 2026-09-06, npm 12.0.2, both prebuild states), so the copy-paste repair REPORTS SUCCESS
41
+ * on a still-dead binding. That is worse than printing nothing. The command that heals was
42
+ * added in v4.0.0 but lived only inside the automated chain and was shown to nobody.
43
+ *
44
+ * This is the same defect the CHANGELOG records once already: the hints used to say
45
+ * `--build-from-source`, which no-oped the same way, and were changed to the flag above.
46
+ * That fix was right for better-sqlite3 12 and expired when the dependency was bumped.
47
+ *
48
+ * `&&`, never `||`: step 1 exits 0 whether or not it did anything, so an `||` chain would
49
+ * never reach step 2 — the no-op is the whole trap. Sequencing unconditionally costs a
50
+ * recompile in the case step 1 already fixed, which is the right trade for a manual repair.
51
+ *
52
+ * @param {string} root Directory whose node_modules holds better-sqlite3
53
+ * @returns {string}
54
+ */
55
+ export function nativeBindingRepairHint(root) {
56
+ // Quoted: INSTALL_DIR / a plugin-cache root can contain spaces, and an unquoted `cd`
57
+ // hands the user a command that fails on exactly the machines least able to debug it.
58
+ return `cd "${root}" && ${NATIVE_BINDING_REBUILD_CMD} && ${NATIVE_BINDING_SOURCE_BUILD_CMD}`;
59
+ }
60
+
32
61
  // Set on a re-exec'd child so one failed heal cannot fork-bomb the CLI.
33
62
  export const BINDING_HEAL_GUARD_ENV = 'CLAUDE_MEM_BINDING_HEALED';
34
63
 
@@ -243,9 +272,24 @@ export async function ensureBetterSqlite3Working(installDir, deps = {}) {
243
272
  // get it appended — otherwise an injected strategy would silently grow a step its owner
244
273
  // never asked for, and (as the stub tests caught) a test that stubs `rebuild` without
245
274
  // stubbing `exec` would shell out for real.
275
+ //
276
+ // `sourceBuild: false` is the EXPLICIT opt-out, and it exists because the `deps.rebuild`
277
+ // test above was the wrong proxy for "this caller has a budget" (A20260906-R8b-P0-1, found
278
+ // by independent review). The one caller that is genuinely time-boxed — the SessionStart
279
+ // hook probe — injects `exec` with a 20 s cap and does NOT inject `rebuild`, so it landed
280
+ // outside the guard. That matters because this step is `node-gyp clean && node-gyp rebuild`:
281
+ // it DELETES build/ before compiling, so a truncated attempt is not a no-op, it is
282
+ // destructive. Measured on a tree whose compiled binding opened a DB — 20 s cap, SIGTERM at
283
+ // 20.02 s, no `.node` left, `DB opens: YES` → `NO`, repeating on every SessionStart because
284
+ // setup.sh writes its marker only on success. Callers with a time budget must opt out and
285
+ // leave the compile to a foreground path (`rebuild-binding`, `healAndReexec`) that has none.
246
286
  const sourceBuild =
247
- deps.sourceBuild ||
248
- (deps.rebuild ? null : () => exec(NATIVE_BINDING_SOURCE_BUILD_CMD, { cwd: installDir, stdio: 'pipe' }));
287
+ deps.sourceBuild === false
288
+ ? null
289
+ : deps.sourceBuild ||
290
+ (deps.rebuild
291
+ ? null
292
+ : () => exec(NATIVE_BINDING_SOURCE_BUILD_CMD, { cwd: installDir, stdio: 'pipe' }));
249
293
  if (!sourceBuild) return { ok: false, error: second.error || first.error };
250
294
 
251
295
  try {
@@ -36,7 +36,7 @@ import { homedir } from 'node:os';
36
36
 
37
37
  import {
38
38
  probeBindingInFreshProcess,
39
- NATIVE_BINDING_REBUILD_CMD,
39
+ nativeBindingRepairHint,
40
40
  flattenBindingError,
41
41
  } from './binding-probe.mjs';
42
42
 
@@ -245,7 +245,7 @@ export function probeRuntimeRoots(roots, deps = {}) {
245
245
  // showed a bare path for the one fault family this check exists for.
246
246
  const error = flattenBindingError(r.error);
247
247
  return ownDeps
248
- ? { label, root, ok: false, error, repair: `cd ${root} && ${NATIVE_BINDING_REBUILD_CMD}` }
248
+ ? { label, root, ok: false, error, repair: nativeBindingRepairHint(root) }
249
249
  : {
250
250
  label,
251
251
  root,