claude-mem-lite 3.70.0 → 3.70.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": "3.70.0",
13
+ "version": "3.70.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": "3.70.0",
3
+ "version": "3.70.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"
@@ -51,6 +51,29 @@ export function isNativeBindingError(err) {
51
51
  return NATIVE_BINDING_PATTERNS.some((re) => re.test(msg));
52
52
  }
53
53
 
54
+ /**
55
+ * Render a binding error as ONE line without losing the diagnosis.
56
+ *
57
+ * Every surface used to do `String(err).split('\n')[0]`, which is exactly wrong for
58
+ * the ABI-mismatch family this subsystem exists to detect. Node's message puts the
59
+ * filename on line 0 and the `NODE_MODULE_VERSION 127 … requires 137` on lines 2-3,
60
+ * so first-line truncation printed a bare path and dropped the only part that says
61
+ * what is wrong. (The comment in probeBindingInFreshProcess below called that string
62
+ * "the highest-value line doctor prints"; for a stale binding it carried no diagnosis
63
+ * at all.) Collapsing whitespace keeps both the path and the numbers while staying
64
+ * safe for a JSON envelope, a JSONL log record and a one-line hook receipt.
65
+ *
66
+ * @param {unknown} err Error, string, or anything thrown.
67
+ * @param {number} [max] Hard cap; a probe must not be able to flood a receipt.
68
+ * @returns {string}
69
+ */
70
+ export function flattenBindingError(err, max = 240) {
71
+ const raw = err instanceof Error ? err.message : err;
72
+ const s = String(raw ?? '').replace(/\s+/g, ' ').trim();
73
+ if (!s) return 'unknown';
74
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
75
+ }
76
+
54
77
  /**
55
78
  * Probe better-sqlite3's native binding by importing it from `installDir`'s
56
79
  * node_modules and opening an in-memory DB. Returns {ok, error?}.
@@ -34,7 +34,7 @@ import { existsSync, readdirSync, realpathSync } from 'node:fs';
34
34
  import { join } from 'node:path';
35
35
  import { homedir } from 'node:os';
36
36
 
37
- import { probeBindingInFreshProcess, NATIVE_BINDING_REBUILD_CMD } from './binding-probe.mjs';
37
+ import { probeBindingInFreshProcess, NATIVE_BINDING_REBUILD_CMD, flattenBindingError } from './binding-probe.mjs';
38
38
 
39
39
  // Module-private: nothing outside needs these, and a new unused export is a
40
40
  // review signal against the knip baseline recorded in CLAUDE.md.
@@ -116,7 +116,7 @@ export function listPluginCacheVersions(opts = {}) {
116
116
  * twice would double every failure message for a single fault.
117
117
  *
118
118
  * @param {{home?: string, projectDir?: string, installDir?: string, marketplace?: string, plugin?: string, pluginRoot?: string}} opts
119
- * @returns {{managed: boolean, pluginVersions: Array<{version: string, root: string}>, activePluginVersion: {version: string, root: string}|null, runtimeRoots: Array<{label: string, root: string, depsMissing?: boolean}>}}
119
+ * @returns {{managed: boolean, pluginVersions: Array<{version: string, root: string}>, activePluginVersion: {version: string, root: string}|null, runtimeRoots: Array<{label: string, root: string, ownDeps: boolean}>}}
120
120
  */
121
121
  export function detectInstallShape({
122
122
  home = homedir(), projectDir, installDir, marketplace, plugin,
@@ -137,31 +137,43 @@ export function detectInstallShape({
137
137
  || null;
138
138
 
139
139
  const runtimeRoots = [];
140
- const byBinding = new Map();
140
+ const byKey = new Map();
141
141
  const add = (label, root, { certified = false } = {}) => {
142
142
  if (!root) return;
143
143
  const bs3 = join(root, 'node_modules', 'better-sqlite3');
144
- if (!existsSync(bs3)) {
145
- // A dir that merely lacks deps is not a runtime root — but one this function
146
- // just CERTIFIED as a code home is. Its hooks and MCP server load from it and
147
- // throw ERR_MODULE_NOT_FOUND on every fire, and that error is not in
148
- // NATIVE_BINDING_PATTERNS, so nothing else records it either. Dropping it
149
- // turned a pre-v3.70 exit 1 into exit 0 (pre-tag review, SHOULD-FIX 1).
150
- if (certified && existsSync(root)) runtimeRoots.push({ label, root, depsMissing: true });
151
- return;
144
+ const ownDeps = existsSync(bs3);
145
+ // A dir that merely lacks deps is not a runtime root — but one this function
146
+ // just CERTIFIED as a code home is, because that is where hooks and the MCP
147
+ // server load from. Dropping it turned a pre-v3.70 exit 1 into exit 0.
148
+ //
149
+ // It is added as a probe TARGET, not pre-judged broken. v3.70.0 pre-judged it,
150
+ // which over-corrected into a false red: Node resolves a specifier up the
151
+ // directory tree, so a code home nested under an ancestor that owns a working
152
+ // better-sqlite3 loads perfectly well. Measured on that build — ground-truth
153
+ // probe {ok:true} against a verdict of "absent, every hook throws". Owning the
154
+ // tree is not the question; being able to LOAD is, and only a probe answers it.
155
+ if (!ownDeps && !(certified && existsSync(root))) return;
156
+
157
+ // Dedup on the binding's realpath when there is one (scripts/setup.sh symlinks a
158
+ // plugin cache's node_modules at the managed dir's, so two homes routinely share
159
+ // one tree and one probe answers for both). With no own tree there is no binding
160
+ // path to key on, so key on the root itself.
161
+ let key = root;
162
+ if (ownDeps) {
163
+ key = bs3;
164
+ try { key = realpathSync(bs3); } catch { /* unresolvable → key on the literal path */ }
165
+ } else {
166
+ try { key = realpathSync(root); } catch { /* ditto */ }
152
167
  }
153
- let key = bs3;
154
- try { key = realpathSync(bs3); } catch { /* unresolvable → dedupe on the literal path */ }
155
- const existing = byBinding.get(key);
168
+ const existing = byKey.get(key);
156
169
  if (existing) {
157
- // Same tree reached through a second home. One probe still answers for
158
- // both, but the label has to say so otherwise a plugin user reading
159
- // "managed install is broken" has no way to know their plugin shares it.
170
+ // The label has to name both homes otherwise a plugin user reading
171
+ // "managed install is broken" has no way to know their plugin shares that tree.
160
172
  existing.label += `, ${label}`;
161
173
  return;
162
174
  }
163
- const entry = { label, root };
164
- byBinding.set(key, entry);
175
+ const entry = { label, root, ownDeps };
176
+ byKey.set(key, entry);
165
177
  runtimeRoots.push(entry);
166
178
  };
167
179
 
@@ -192,27 +204,34 @@ function resolvesSame(a, b) {
192
204
  */
193
205
  export function probeRuntimeRoots(roots, deps = {}) {
194
206
  const probe = deps.probe || ((root) => probeBindingInFreshProcess(root));
195
- return roots.map(({ label, root, depsMissing }) => {
196
- // Nothing to dlopen: the tree is absent, not stale. Say so and prescribe an
197
- // install `npm rebuild` on a missing package exits 0 and heals nothing.
198
- if (depsMissing) {
199
- return {
207
+ return roots.map(({ label, root, ownDeps = true }) => {
208
+ // EVERY root is probed, including one with no tree of its own: the probe uses
209
+ // Node's real resolution chain, so it is the only thing that knows whether an
210
+ // ancestor node_modules is carrying this install. Pre-judging an unowned tree
211
+ // broken is what made v3.70.0 report "absent — every hook throws" about a root
212
+ // that loaded fine.
213
+ const r = probe(root);
214
+ if (r.ok) return { label, root, ok: true };
215
+ // The repair depends on WHICH failure it is. `npm rebuild` on a package that is
216
+ // not installed exits 0 and heals nothing, so an unowned tree that also cannot
217
+ // resolve from an ancestor needs an install; a present-but-unloadable tree needs
218
+ // the rebuild.
219
+ // NOT `.split('\n')[0]`: Node puts the filename on line 0 and the
220
+ // `NODE_MODULE_VERSION 127 … requires 137` on lines 2-3, so first-line truncation
221
+ // showed a bare path for the one fault family this check exists for.
222
+ const error = flattenBindingError(r.error);
223
+ return ownDeps
224
+ ? { label, root, ok: false, error, repair: `cd ${root} && ${NATIVE_BINDING_REBUILD_CMD}` }
225
+ : {
200
226
  label,
201
227
  root,
202
228
  ok: false,
203
- error: 'node_modules/better-sqlite3 is absent every hook and the MCP server '
204
- + 'that load this install throw ERR_MODULE_NOT_FOUND',
229
+ // Say only what is known. The probe may have reached an ANCESTOR
230
+ // node_modules and failed there — an earlier draft of this line asserted
231
+ // "none resolvable from a parent directory" and was caught printing that
232
+ // next to a probe error naming the parent tree it had just loaded from.
233
+ error: `${error} — and this install owns no node_modules/better-sqlite3, so there is nothing here to rebuild`,
205
234
  repair: `cd ${root} && npm install --omit=dev`,
206
235
  };
207
- }
208
- const r = probe(root);
209
- if (r.ok) return { label, root, ok: true };
210
- return {
211
- label,
212
- root,
213
- ok: false,
214
- error: String(r.error || 'unknown').split('\n')[0],
215
- repair: `cd ${root} && ${NATIVE_BINDING_REBUILD_CMD}`,
216
- };
217
236
  });
218
237
  }
@@ -24,7 +24,7 @@
24
24
  import { join } from 'node:path';
25
25
  import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
26
26
  import { fileURLToPath } from 'node:url';
27
- import { isNativeBindingError } from './binding-probe.mjs';
27
+ import { isNativeBindingError, flattenBindingError } from './binding-probe.mjs';
28
28
 
29
29
  export const NATIVE_BINDING_HINT_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
30
30
  const MARKER_NAME = 'native-binding-hint-last';
@@ -136,7 +136,7 @@ export function recordNativeBindingBreakage(runtimeDir, { reason = '', event = '
136
136
  const tmp = `${marker}.tmp-${process.pid}`;
137
137
  // First line only: the ABI error is multi-line and the marker is read by the
138
138
  // launcher (pure node:, no parser beyond JSON.parse) and by `doctor`.
139
- writeFileSync(tmp, JSON.stringify({ reason: String(reason).split('\n')[0], event, ts: now }));
139
+ writeFileSync(tmp, JSON.stringify({ reason: flattenBindingError(reason), event, ts: now }));
140
140
  renameSync(tmp, marker);
141
141
  } catch { /* best-effort */ }
142
142
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.70.0",
3
+ "version": "3.70.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.70.0",
9
+ "version": "3.70.2",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.26.0",
12
12
  "better-sqlite3": "^12.6.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.70.0",
3
+ "version": "3.70.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
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -42,8 +42,16 @@ function bareProbe(root) {
42
42
  const r = spawnSync(process.execPath, ['-e', script], { stdio: 'pipe', timeout: 8000 });
43
43
  if (!r.error && r.status === 0) return true;
44
44
  // Say WHY. The inline predecessor printed the cause here; dropping it left the
45
- // user with setup.sh's generic "binding unusable" and nothing to act on.
46
- const why = String(r.stdout || '').trim().split('\n')[0]
45
+ // user with setup.sh's generic "binding unusable" and nothing to act on. Flattened
46
+ // rather than first-lined: Node's ABI message puts the filename on line 0 and the
47
+ // NODE_MODULE_VERSION pair on lines 2-3, so `.split('\n')[0]` said WHERE but never
48
+ // WHY — for the exact fault this probe exists to find.
49
+ //
50
+ // Flattening is inlined, NOT lib/binding-probe.mjs::flattenBindingError, because
51
+ // this function is the fallback for a tree where lib/ failed to import — `helpers`
52
+ // is still null on every path that reaches here. Duplicated deliberately; keep the
53
+ // two in step.
54
+ const why = String(r.stdout || '').replace(/\s+/g, ' ').trim().slice(0, 240)
47
55
  || (r.error && r.error.message)
48
56
  || `probe exited ${r.status ?? `on signal ${r.signal}`}`;
49
57
  process.stderr.write(`[claude-mem-lite] binding probe: ${why}\n`);
@@ -82,9 +90,9 @@ try {
82
90
  }
83
91
  const release = helpers.acquireLock(lockPath);
84
92
  if (!release) {
85
- const firstLine = String(first.error).split('\n')[0];
86
93
  process.stderr.write(
87
- `[claude-mem-lite] binding probe: ${firstLine} (another install/repair in flight — deferring heal)\n`,
94
+ `[claude-mem-lite] binding probe: ${helpers.flattenBindingError(first.error)} `
95
+ + '(another install/repair in flight — deferring heal)\n',
88
96
  );
89
97
  process.exit(1);
90
98
  }