claude-mem-lite 2.83.1 → 2.84.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.83.1",
13
+ "version": "2.84.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.83.1",
3
+ "version": "2.84.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/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'citation-stats', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'import-jsonl', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
3
- const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'release']);
3
+ const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'repair', 'release']);
4
4
 
5
5
  const cmd = process.argv[2];
6
6
 
package/hook-update.mjs CHANGED
@@ -5,10 +5,14 @@
5
5
  import { execSync, execFileSync } from 'node:child_process';
6
6
  import { readFileSync, writeFileSync, copyFileSync, cpSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync, chmodSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
8
9
  import { tmpdir, homedir } from 'node:os';
9
10
  import { DB_DIR } from './schema.mjs';
10
11
  import { debugCatch, debugLog } from './utils.mjs';
11
- import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
12
+ // Local manifest is fallback only — the active manifest is loaded from the
13
+ // extracted tarball's own source-files.mjs inside installExtractedRelease.
14
+ // See loadReleaseManifest below.
15
+ import { SOURCE_FILES as LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES as LOCAL_HOOK_SCRIPT_FILES } from './source-files.mjs';
12
16
 
13
17
  // ── Configuration ──────────────────────────────────────────
14
18
  const GITHUB_REPO = 'sdsrss/claude-mem-lite';
@@ -192,11 +196,42 @@ export function getCurrentVersion() {
192
196
  } catch { return '0.0.0'; }
193
197
  }
194
198
 
195
- // Source files imported from shared ./source-files.mjs so install.mjs and
196
- // hook-update.mjs can never drift (see tests/source-files-sync.test.mjs).
197
199
  // SWITCHABLE_PATHS = everything in SOURCE_FILES plus the recursive dirs that
198
- // install.mjs copies as whole subtrees (scripts, registry, node_modules).
199
- const SWITCHABLE_PATHS = [...SOURCE_FILES, 'scripts', 'registry', 'node_modules'];
200
+ // install.mjs copies as whole subtrees (scripts, registry, node_modules). It's
201
+ // built per-call from the *tarball's* manifest, not the locally-imported one —
202
+ // see loadReleaseManifest comment for why.
203
+ function buildSwitchablePaths(sourceFiles) {
204
+ return [...sourceFiles, 'scripts', 'registry', 'node_modules'];
205
+ }
206
+
207
+ // Load the SOURCE_FILES / HOOK_SCRIPT_FILES manifest from the *extracted
208
+ // tarball's* own source-files.mjs. Critical: the locally-imported
209
+ // LOCAL_SOURCE_FILES is frozen at install time, so any entry added in the
210
+ // release we're installing is invisible to the running update. Pre-fix
211
+ // (≤ v2.83.2) used LOCAL_SOURCE_FILES for both copyReleaseIntoStaging and
212
+ // SWITCHABLE_PATHS — v2.80.x → v2.81.0 auto-update copied the new hook.mjs
213
+ // (in the v2.80 manifest) but skipped lib/cite-back-hint.mjs (added in v2.81),
214
+ // breaking SessionStart on every machine that auto-updated and killing the
215
+ // hook chain that would otherwise self-heal on the next round.
216
+ async function loadReleaseManifest(sourceDir) {
217
+ const manifestPath = join(sourceDir, 'source-files.mjs');
218
+ if (!existsSync(manifestPath)) {
219
+ return { SOURCE_FILES: LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES: LOCAL_HOOK_SCRIPT_FILES, source: 'fallback-missing' };
220
+ }
221
+ try {
222
+ const mod = await import(pathToFileURL(manifestPath).href + `?t=${Date.now()}`);
223
+ if (!Array.isArray(mod.SOURCE_FILES) || mod.SOURCE_FILES.length === 0) {
224
+ throw new Error('SOURCE_FILES missing or empty');
225
+ }
226
+ if (!Array.isArray(mod.HOOK_SCRIPT_FILES)) {
227
+ throw new Error('HOOK_SCRIPT_FILES missing');
228
+ }
229
+ return { SOURCE_FILES: mod.SOURCE_FILES, HOOK_SCRIPT_FILES: mod.HOOK_SCRIPT_FILES, source: 'tarball' };
230
+ } catch (e) {
231
+ debugCatch(e, 'loadReleaseManifest');
232
+ return { SOURCE_FILES: LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES: LOCAL_HOOK_SCRIPT_FILES, source: 'fallback-error' };
233
+ }
234
+ }
200
235
 
201
236
  // ── Download & Install ─────────────────────────────────────
202
237
  // Direct file copy instead of running old install.mjs (avoids symlink overwrite in dev)
@@ -223,7 +258,7 @@ async function downloadAndInstall(tarballUrl, expectedVersion) {
223
258
  return false;
224
259
  }
225
260
 
226
- return installExtractedRelease(tmpDir);
261
+ return await installExtractedRelease(tmpDir);
227
262
  } catch (err) {
228
263
  debugCatch(err, 'downloadAndInstall');
229
264
  return false;
@@ -271,25 +306,28 @@ export function validateExtractedTarball(sourceDir, expectedVersion, expectedNam
271
306
  return { ok: true };
272
307
  }
273
308
 
274
- export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
309
+ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
275
310
  const ts = `${Date.now()}-${process.pid}`;
276
311
  const stagingDir = join(targetDir, `.update-staging-${ts}`);
277
312
  const backupDir = join(targetDir, `.update-backup-${ts}`);
278
313
  const backedUp = [];
279
314
  const installed = [];
280
315
 
316
+ const manifest = await loadReleaseManifest(sourceDir);
317
+ const switchablePaths = buildSwitchablePaths(manifest.SOURCE_FILES);
318
+
281
319
  try {
282
320
  mkdirSync(stagingDir, { recursive: true });
283
321
  mkdirSync(backupDir, { recursive: true });
284
322
 
285
- copyReleaseIntoStaging(sourceDir, stagingDir);
323
+ copyReleaseIntoStaging(sourceDir, stagingDir, manifest);
286
324
  execSync(NPM_INSTALL_CMD, {
287
325
  cwd: stagingDir,
288
326
  timeout: 60000,
289
327
  stdio: 'pipe',
290
328
  });
291
329
 
292
- for (const relPath of SWITCHABLE_PATHS) {
330
+ for (const relPath of switchablePaths) {
293
331
  const stagedPath = join(stagingDir, relPath);
294
332
  if (!existsSync(stagedPath)) continue;
295
333
 
@@ -368,10 +406,10 @@ export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
368
406
  }
369
407
  }
370
408
 
371
- function copyReleaseIntoStaging(sourceDir, stagingDir) {
409
+ function copyReleaseIntoStaging(sourceDir, stagingDir, manifest = { SOURCE_FILES: LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES: LOCAL_HOOK_SCRIPT_FILES }) {
372
410
  let copied = 0;
373
411
 
374
- for (const f of SOURCE_FILES) {
412
+ for (const f of manifest.SOURCE_FILES) {
375
413
  const src = join(sourceDir, f);
376
414
  const dest = join(stagingDir, f);
377
415
  if (!existsSync(src)) continue;
@@ -389,7 +427,7 @@ function copyReleaseIntoStaging(sourceDir, stagingDir) {
389
427
  const sourceScripts = join(sourceDir, 'scripts');
390
428
  if (existsSync(sourceScripts)) {
391
429
  mkdirSync(stagingScripts, { recursive: true });
392
- for (const name of HOOK_SCRIPT_FILES) {
430
+ for (const name of manifest.HOOK_SCRIPT_FILES) {
393
431
  const src = join(sourceScripts, name);
394
432
  if (existsSync(src)) copyFileSync(src, join(stagingScripts, name));
395
433
  }
package/hooks/hooks.json CHANGED
@@ -12,7 +12,7 @@
12
12
  },
13
13
  {
14
14
  "type": "command",
15
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" session-start",
15
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" hook.mjs session-start",
16
16
  "timeout": 15
17
17
  }
18
18
  ]
@@ -24,7 +24,7 @@
24
24
  "hooks": [
25
25
  {
26
26
  "type": "command",
27
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" pre-compact",
27
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" hook.mjs pre-compact",
28
28
  "timeout": 5
29
29
  }
30
30
  ]
@@ -36,7 +36,7 @@
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
39
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-recall.js\"",
39
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" scripts/pre-tool-recall.js",
40
40
  "timeout": 3
41
41
  }
42
42
  ]
@@ -46,7 +46,7 @@
46
46
  "hooks": [
47
47
  {
48
48
  "type": "command",
49
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-skill-bridge.js\"",
49
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" scripts/pre-skill-bridge.js",
50
50
  "timeout": 3
51
51
  }
52
52
  ]
@@ -70,7 +70,7 @@
70
70
  "hooks": [
71
71
  {
72
72
  "type": "command",
73
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" stop",
73
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" hook.mjs stop",
74
74
  "timeout": 5
75
75
  }
76
76
  ]
@@ -82,12 +82,12 @@
82
82
  "hooks": [
83
83
  {
84
84
  "type": "command",
85
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-search.js\"",
85
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" scripts/user-prompt-search.js",
86
86
  "timeout": 2
87
87
  },
88
88
  {
89
89
  "type": "command",
90
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" user-prompt",
90
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" hook.mjs user-prompt",
91
91
  "timeout": 5
92
92
  }
93
93
  ]
package/install.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { execSync, execFileSync } from 'child_process';
5
5
  import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, cpSync, renameSync, symlinkSync, unlinkSync, readdirSync, statSync, lstatSync } from 'fs';
6
6
  import { join, resolve, dirname, isAbsolute } from 'path';
7
- import { homedir } from 'os';
7
+ import { homedir, tmpdir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
9
 
10
10
  const PROJECT_DIR = resolve(import.meta.dirname ?? dirname(fileURLToPath(import.meta.url)));
@@ -553,7 +553,13 @@ async function install() {
553
553
  }
554
554
  settings.hooks = settings.hooks || {};
555
555
 
556
- const PREFILTER_PATH = join(INSTALL_DIR, 'scripts', 'post-tool-use.sh');
556
+ const SCRIPTS_PATH = join(INSTALL_DIR, 'scripts');
557
+ const PREFILTER_PATH = join(SCRIPTS_PATH, 'post-tool-use.sh');
558
+ // v2.84: every Node hook invocation routes through hook-launcher.mjs so an
559
+ // ERR_MODULE_NOT_FOUND from a partial-install drift auto-heals via
560
+ // install.mjs repair instead of permanently bricking the hook chain.
561
+ const LAUNCHER_PATH = join(SCRIPTS_PATH, 'hook-launcher.mjs');
562
+ const nodeHook = (entry, ...args) => `node "${LAUNCHER_PATH}" ${entry} ${args.join(' ')}`.trim();
557
563
 
558
564
  const memPostToolUse = {
559
565
  matcher: '*',
@@ -568,7 +574,7 @@ async function install() {
568
574
  matcher: 'startup|clear|compact',
569
575
  hooks: [{
570
576
  type: 'command',
571
- command: `node "${HOOK_PATH}" session-start`,
577
+ command: nodeHook('hook.mjs', 'session-start'),
572
578
  timeout: 10
573
579
  }]
574
580
  };
@@ -577,24 +583,22 @@ async function install() {
577
583
  matcher: '*',
578
584
  hooks: [{
579
585
  type: 'command',
580
- command: `node "${HOOK_PATH}" stop`,
586
+ command: nodeHook('hook.mjs', 'stop'),
581
587
  timeout: 5
582
588
  }]
583
589
  };
584
590
 
585
- const SCRIPTS_PATH = join(INSTALL_DIR, 'scripts');
586
-
587
591
  const memUserPrompt = {
588
592
  matcher: '*',
589
593
  hooks: [
590
594
  {
591
595
  type: 'command',
592
- command: `node "${join(SCRIPTS_PATH, 'user-prompt-search.js')}"`,
596
+ command: nodeHook('scripts/user-prompt-search.js'),
593
597
  timeout: 2
594
598
  },
595
599
  {
596
600
  type: 'command',
597
- command: `node "${HOOK_PATH}" user-prompt`,
601
+ command: nodeHook('hook.mjs', 'user-prompt'),
598
602
  timeout: 5
599
603
  }
600
604
  ]
@@ -608,7 +612,7 @@ async function install() {
608
612
  hooks: [
609
613
  {
610
614
  type: 'command',
611
- command: `node "${join(SCRIPTS_PATH, 'pre-tool-recall.js')}"`,
615
+ command: nodeHook('scripts/pre-tool-recall.js'),
612
616
  timeout: 3
613
617
  }
614
618
  ]
@@ -619,7 +623,7 @@ async function install() {
619
623
  hooks: [
620
624
  {
621
625
  type: 'command',
622
- command: `node "${join(SCRIPTS_PATH, 'pre-skill-bridge.js')}"`,
626
+ command: nodeHook('scripts/pre-skill-bridge.js'),
623
627
  timeout: 3
624
628
  }
625
629
  ]
@@ -1758,6 +1762,45 @@ async function manualUpdate() {
1758
1762
  console.log('');
1759
1763
  }
1760
1764
 
1765
+ // ─── Repair: Re-sync from latest GitHub Release ─────────────────────────────
1766
+ // Recovery path for installs broken by a partial auto-update (most often the
1767
+ // stale-manifest bug fixed in v2.84.0: hook-update.mjs copied the new hook.mjs
1768
+ // but skipped a new lib/* entry, leaving an ERR_MODULE_NOT_FOUND that
1769
+ // permanently disables the hook chain — including the next auto-update that
1770
+ // would have healed it). Self-contained: downloads a fresh tarball and spawns
1771
+ // the tarball's own install.mjs install, so the recovery path always runs the
1772
+ // latest code even when local install.mjs / hook-update.mjs are themselves
1773
+ // buggy on disk.
1774
+ async function repair() {
1775
+ console.log('\nclaude-mem-lite repair — re-syncing from latest GitHub release\n');
1776
+ const stagingDir = join(tmpdir(), `claude-mem-lite-repair-${Date.now()}`);
1777
+ mkdirSync(stagingDir, { recursive: true });
1778
+ try {
1779
+ const tarballUrl = 'https://api.github.com/repos/sdsrss/claude-mem-lite/tarball';
1780
+ const tarballPath = join(stagingDir, 'release.tgz');
1781
+ log('Downloading latest release tarball...');
1782
+ execFileSync('curl', ['-sL', '-f', '-H', 'Accept: application/vnd.github+json', tarballUrl, '-o', tarballPath],
1783
+ { timeout: 60000, stdio: ['ignore', 'pipe', 'inherit'] });
1784
+ log('Extracting...');
1785
+ execFileSync('tar', ['xzf', tarballPath, '-C', stagingDir, '--strip-components=1'],
1786
+ { timeout: 30000, stdio: ['ignore', 'pipe', 'inherit'] });
1787
+ const tarballInstaller = join(stagingDir, 'install.mjs');
1788
+ if (!existsSync(tarballInstaller)) {
1789
+ fail('Tarball missing install.mjs — repair aborted');
1790
+ process.exit(1);
1791
+ }
1792
+ log('Re-running install from freshly-downloaded sources...');
1793
+ execFileSync(process.execPath, [tarballInstaller, 'install'],
1794
+ { stdio: 'inherit', timeout: 300000 });
1795
+ ok('Repair complete — broken install resynced from latest release');
1796
+ } catch (e) {
1797
+ fail(`Repair failed: ${e.message}`);
1798
+ process.exit(1);
1799
+ } finally {
1800
+ try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
1801
+ }
1802
+ }
1803
+
1761
1804
  // ─── Release: Sync Versions ─────────────────────────────────────────────────
1762
1805
 
1763
1806
  function syncVersions() {
@@ -1860,6 +1903,9 @@ export async function main(argv = process.argv.slice(2)) {
1860
1903
  case 'update':
1861
1904
  await manualUpdate();
1862
1905
  break;
1906
+ case 'repair':
1907
+ await repair();
1908
+ break;
1863
1909
  case 'release':
1864
1910
  syncVersions();
1865
1911
  if (!flags.has('--no-lock')) regenerateLockfile();
@@ -1889,6 +1935,7 @@ Usage:
1889
1935
  node install.mjs cleanup Remove stale temp/staging files (use --dry-run to preview)
1890
1936
  node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
1891
1937
  node install.mjs self-update Check for and install updates
1938
+ node install.mjs repair Recover a broken install: download latest tarball, re-run install
1892
1939
  node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
1893
1940
 
1894
1941
  npx claude-mem-lite Install via npx (one-liner)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.83.1",
3
+ "version": "2.84.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -119,6 +119,7 @@
119
119
  "scripts/pre-tool-recall.js",
120
120
  "scripts/pre-skill-bridge.js",
121
121
  "scripts/prompt-search-utils.mjs",
122
+ "scripts/hook-launcher.mjs",
122
123
  ".mcp.json",
123
124
  ".claude-plugin/plugin.json",
124
125
  ".claude-plugin/marketplace.json",
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // scripts/hook-launcher.mjs — Self-healing wrapper for Node hook entry points.
3
+ //
4
+ // Why: pre-v2.84 a stale-manifest bug in hook-update.mjs could leave the
5
+ // install with a hook.mjs that imports lib/cite-back-hint.mjs (or any other
6
+ // newly-added module) while the file itself was never copied. The resulting
7
+ // ERR_MODULE_NOT_FOUND killed every hook fire, including the next auto-update
8
+ // that would have healed the install. v2.84.0 fixes the root cause; this
9
+ // launcher is defense-in-depth for similar future drift (corrupt download,
10
+ // half-applied install, manual file deletion).
11
+ //
12
+ // Behavior: try-import the target entry. On ERR_MODULE_NOT_FOUND whose URL
13
+ // points under the install dir, run `install.mjs repair` (rate-limited via a
14
+ // 6h marker file under runtime/) and retry the import once. On any other
15
+ // exception, re-throw so Node's default error surface is preserved.
16
+ //
17
+ // HARD constraint: pure node: imports only. Importing anything from lib/ here
18
+ // would defeat the entire purpose — the launcher must survive a broken
19
+ // install.
20
+
21
+ import { existsSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
22
+ import { spawnSync } from 'node:child_process';
23
+ import { dirname, join } from 'node:path';
24
+ import { fileURLToPath, pathToFileURL } from 'node:url';
25
+ import { homedir } from 'node:os';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const INSTALL_DIR = join(__dirname, '..');
29
+ const RUNTIME_DIR = process.env.CLAUDE_MEM_DIR
30
+ ? join(process.env.CLAUDE_MEM_DIR, 'runtime')
31
+ : join(homedir(), '.claude-mem-lite', 'runtime');
32
+ const HEAL_MARKER = join(RUNTIME_DIR, 'hook-launcher-lastheal');
33
+ const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
34
+
35
+ const [, , entryArg, ...rest] = process.argv;
36
+ if (!entryArg) {
37
+ process.stderr.write('[claude-mem-lite] hook-launcher: missing entry argument\n');
38
+ process.exit(1);
39
+ }
40
+
41
+ const entryAbs = entryArg.startsWith('/') ? entryArg : join(INSTALL_DIR, entryArg);
42
+
43
+ async function runEntry({ bustCache = false } = {}) {
44
+ // Mirror direct invocation: process.argv[1] is the entry, [2..] are its args.
45
+ process.argv = [process.argv[0], entryAbs, ...rest];
46
+ // Node ESM caches resolution outcomes (success AND failure) by URL. On the
47
+ // post-self-heal retry the freshly-written module file lives at the same
48
+ // path the first import already cached as ERR_MODULE_NOT_FOUND — without a
49
+ // cache-buster query the second await import() returns the cached rejection
50
+ // and the heal looks like it did nothing.
51
+ const url = pathToFileURL(entryAbs).href + (bustCache ? `?t=${Date.now()}` : '');
52
+ await import(url);
53
+ }
54
+
55
+ function isLocalModuleErr(e) {
56
+ if (!e || e.code !== 'ERR_MODULE_NOT_FOUND') return false;
57
+ const where = String(e.url || e.message || '');
58
+ return where.includes('.claude-mem-lite') || where.startsWith(`file://${INSTALL_DIR}`);
59
+ }
60
+
61
+ function recentHealAttempt() {
62
+ try {
63
+ return Date.now() - statSync(HEAL_MARKER).mtimeMs < HEAL_COOLDOWN_MS;
64
+ } catch { return false; }
65
+ }
66
+
67
+ function recordHealAttempt() {
68
+ try {
69
+ mkdirSync(RUNTIME_DIR, { recursive: true });
70
+ writeFileSync(HEAL_MARKER, String(Date.now()));
71
+ } catch { /* best-effort */ }
72
+ }
73
+
74
+ async function attemptHeal(reason) {
75
+ if (recentHealAttempt()) {
76
+ process.stderr.write(
77
+ `[claude-mem-lite] Self-heal skipped (last attempt < 6h ago). Manual recovery: claude-mem-lite repair\n`,
78
+ );
79
+ return false;
80
+ }
81
+ recordHealAttempt();
82
+ process.stderr.write(`[claude-mem-lite] Detected broken install (${reason}) — running self-heal\n`);
83
+ const installer = join(INSTALL_DIR, 'install.mjs');
84
+ if (!existsSync(installer)) {
85
+ process.stderr.write(`[claude-mem-lite] install.mjs missing at ${installer} — cannot self-heal\n`);
86
+ return false;
87
+ }
88
+ const result = spawnSync(process.execPath, [installer, 'repair'], {
89
+ stdio: 'inherit',
90
+ timeout: 300000,
91
+ });
92
+ return result.status === 0;
93
+ }
94
+
95
+ try {
96
+ await runEntry();
97
+ } catch (e) {
98
+ if (!isLocalModuleErr(e)) throw e;
99
+ const reason = String(e.url || e.message).split('/').slice(-2).join('/');
100
+ const healed = await attemptHeal(reason);
101
+ if (!healed) throw e;
102
+ try {
103
+ await runEntry({ bustCache: true });
104
+ } catch (retryErr) {
105
+ process.stderr.write(`[claude-mem-lite] Hook still failing after self-heal: ${retryErr.message}\n`);
106
+ process.exit(1);
107
+ }
108
+ }
@@ -56,6 +56,7 @@ case "$tool" in
56
56
  ;;
57
57
  esac
58
58
 
59
- # Tool not skipped — hand off to Node for full processing
59
+ # Tool not skipped — hand off to Node for full processing.
60
+ # Routed through hook-launcher.mjs (self-heal on ERR_MODULE_NOT_FOUND).
60
61
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 1
61
- printf '%s' "$input" | node "${SCRIPT_DIR}/hook.mjs" post-tool-use
62
+ printf '%s' "$input" | node "${SCRIPT_DIR}/scripts/hook-launcher.mjs" hook.mjs post-tool-use
@@ -9,6 +9,7 @@ import { basename, join } from 'path';
9
9
  import { homedir } from 'os';
10
10
  import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
11
11
  import { recordHookError } from '../lib/hook-telemetry.mjs';
12
+ import { citeFactorClause } from '../scoring-sql.mjs';
12
13
 
13
14
  // CLAUDE_MEM_DIR matches schema.mjs / main CLI — one env var sandboxes the
14
15
  // whole system. CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR remain as
@@ -232,6 +233,11 @@ try {
232
233
  OR (o.type IN ('bugfix', 'decision') AND ${notLowSignalSql})
233
234
  )`;
234
235
  const obsLimit = isRead ? 1 : 2;
236
+ // A1.5 (v2.83.2): cite_factor as a tertiary sort key. When multiple file-
237
+ // matching lessons exist, the one with proven cite history outranks the
238
+ // merely-most-recent one. Single-match files unchanged (obsLimit=1 Read /
239
+ // 2 Edit). Composes with v2.83.0 A1 to extend the citation-decay feedback
240
+ // loop to the 85%-recall PreToolUse:Read/Edit path.
235
241
  const rows = db.prepare(`
236
242
  SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned
237
243
  FROM observations o
@@ -245,6 +251,7 @@ try {
245
251
  ${typeFallback}
246
252
  ORDER BY
247
253
  CASE WHEN o.lesson_learned IS NOT NULL AND o.lesson_learned != '' THEN 0 ELSE 1 END,
254
+ ${citeFactorClause('o')} DESC,
248
255
  o.created_at_epoch DESC
249
256
  LIMIT ${obsLimit}
250
257
  `).all(project, cutoff, filePath, likePattern);
package/source-files.mjs CHANGED
@@ -110,4 +110,9 @@ export const HOOK_SCRIPT_FILES = [
110
110
  'prompt-search-utils.mjs',
111
111
  'pre-tool-recall.js',
112
112
  'pre-skill-bridge.js',
113
+ // v2.84: self-heal wrapper that detects ERR_MODULE_NOT_FOUND under the
114
+ // install dir and runs install.mjs repair before retrying the entry.
115
+ // hooks.json + install.mjs settings template invoke node hook entries
116
+ // through this wrapper so any partial-install drift heals automatically.
117
+ 'hook-launcher.mjs',
113
118
  ];