claude-mem-lite 3.99.0 → 4.0.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.99.0",
13
+ "version": "4.0.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.99.0",
3
+ "version": "4.0.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/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,10 +11,51 @@ 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
 
20
+ // Last-resort heal, added in v4.0.0 with better-sqlite3 13. THE npm-REBUILD PATH ABOVE
21
+ // CANNOT HEAL v13 AT ALL — measured, not inferred: 12 carried
22
+ // `"install": "prebuild-install || node-gyp rebuild --release"`, and 13 carries NO install
23
+ // script whatsoever (it ships `prebuilds/<platform>.node` instead). `npm rebuild` therefore
24
+ // has nothing to run and exits 0 printing "rebuilt dependencies successfully" while
25
+ // producing no `.node` — the same print-success-compile-nothing trap the constant above was
26
+ // written for, moved up one level and now immune to the --dangerously-allow-all-scripts
27
+ // flag. On the 8 platforms 13 prebuilds (linux/linuxmusl/darwin/win32 × x64/arm64) the heal
28
+ // never runs. On any other platform this is the only remaining recovery: the package still
29
+ // ships `src/`, `deps/` and `binding.gyp`, so its own build-release script compiles from
30
+ // source. Verified in a sandbox: prebuilds+build removed → `npm rebuild …` exits 0 and heals
31
+ // nothing → this command produces build/Release/better_sqlite3.node and the DB opens.
32
+ export const NATIVE_BINDING_SOURCE_BUILD_CMD = 'npm run --prefix node_modules/better-sqlite3 build-release';
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
+ return `cd ${root} && ${NATIVE_BINDING_REBUILD_CMD} && ${NATIVE_BINDING_SOURCE_BUILD_CMD}`;
57
+ }
58
+
18
59
  // Set on a re-exec'd child so one failed heal cannot fork-bomb the CLI.
19
60
  export const BINDING_HEAL_GUARD_ENV = 'CLAUDE_MEM_BINDING_HEALED';
20
61
 
@@ -218,7 +259,32 @@ export async function ensureBetterSqlite3Working(installDir, deps = {}) {
218
259
  const second = await verify();
219
260
  if (second.ok) return { ok: true, action: 'rebuilt' };
220
261
 
221
- return { ok: false, error: second.error || first.error };
262
+ // Source-compile fallback (v4.0.0). Reached only when the npm path ran without throwing
263
+ // and STILL left an unusable binding — which is exactly what better-sqlite3 13 does on a
264
+ // platform it ships no prebuild for, because it has no install script for `npm rebuild`
265
+ // to run. Deliberately gated behind a failed verify rather than replacing the npm path:
266
+ // on 12, and on 13 wherever a prebuild matches, the npm path already worked and this
267
+ // never fires.
268
+ //
269
+ // It is part of OUR rebuild strategy, so a caller that injected its own `rebuild` does not
270
+ // get it appended — otherwise an injected strategy would silently grow a step its owner
271
+ // never asked for, and (as the stub tests caught) a test that stubs `rebuild` without
272
+ // stubbing `exec` would shell out for real.
273
+ const sourceBuild =
274
+ deps.sourceBuild ||
275
+ (deps.rebuild ? null : () => exec(NATIVE_BINDING_SOURCE_BUILD_CMD, { cwd: installDir, stdio: 'pipe' }));
276
+ if (!sourceBuild) return { ok: false, error: second.error || first.error };
277
+
278
+ try {
279
+ await sourceBuild();
280
+ } catch (e) {
281
+ return { ok: false, error: `source build failed: ${e.message}` };
282
+ }
283
+
284
+ const third = await verify();
285
+ if (third.ok) return { ok: true, action: 'compiled' };
286
+
287
+ return { ok: false, error: third.error || second.error || first.error };
222
288
  }
223
289
 
224
290
  /**
@@ -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,