claude-mem-lite 2.83.2 → 2.84.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": "2.83.2",
13
+ "version": "2.84.1",
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.2",
3
+ "version": "2.84.1",
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/README.md CHANGED
@@ -498,6 +498,24 @@ Checks Node.js version, dependencies, server/hook files, database integrity, FTS
498
498
 
499
499
  Shows MCP registration, hook configuration, plugin disabled state, and database stats (observation/session counts).
500
500
 
501
+ ### Recovery (stuck install / hook errors)
502
+
503
+ If you see `ERR_MODULE_NOT_FOUND` on PreToolUse:Read/Edit/Skill hooks, or `claude-mem-lite` commands crash with import errors, you're likely hit by a partial auto-update — the updater copied new scripts but missed a sibling `lib/*` file, breaking the hook chain (and the next auto-update that would have healed it).
504
+
505
+ **v2.84.0+** ships a `repair` subcommand that re-syncs from the latest GitHub release:
506
+
507
+ ```bash
508
+ claude-mem-lite repair
509
+ ```
510
+
511
+ **If `repair` itself fails** (the bin is older than v2.84.0, or the bin is also broken), run this one-liner — it pulls a fresh tarball into a temp dir and runs *that* tarball's `install.mjs`, bypassing every file on your disk:
512
+
513
+ ```bash
514
+ T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
515
+ ```
516
+
517
+ After it finishes, `~/.claude-mem-lite/` is back in sync with the latest release and `claude-mem-lite repair` is available for next time.
518
+
501
519
  ## Uninstall
502
520
 
503
521
  ```bash
package/README.zh-CN.md CHANGED
@@ -460,6 +460,24 @@ npx claude-mem-lite doctor # 诊断问题
460
460
 
461
461
  显示 MCP 注册状态、钩子配置、插件禁用状态和数据库统计(观察/会话数量)。
462
462
 
463
+ ### 故障恢复(安装卡死 / hook 报错)
464
+
465
+ 如果你看到 PreToolUse:Read/Edit/Skill hook 报 `ERR_MODULE_NOT_FOUND`,或者 `claude-mem-lite` 命令本身因为 import 错误崩溃,多半是被部分自动更新坑了——更新器复制了新脚本但漏了配套的 `lib/*` 文件,hook 链就此断掉(连下一次本可自愈的自动更新也跑不了)。
466
+
467
+ **v2.84.0+** 提供 `repair` 子命令,从 GitHub 最新 release 重新同步:
468
+
469
+ ```bash
470
+ claude-mem-lite repair
471
+ ```
472
+
473
+ **如果 `repair` 自己也跑不起来**(bin 比 v2.84.0 旧,或 bin 也坏了),用这条单行命令——它把最新 tarball 拉到临时目录、跑 *那份* tarball 里的 `install.mjs`,完全不依赖你磁盘上的任何文件:
474
+
475
+ ```bash
476
+ T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
477
+ ```
478
+
479
+ 跑完之后,`~/.claude-mem-lite/` 就和最新 release 对齐,`claude-mem-lite repair` 下次再遇到类似问题也能直接用了。
480
+
463
481
  ## 卸载
464
482
 
465
483
  ```bash
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,50 @@ 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
+ console.log('');
1799
+ console.log(' Manual fallback — run this in any shell:');
1800
+ console.log('');
1801
+ console.log(' T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install');
1802
+ console.log('');
1803
+ process.exit(1);
1804
+ } finally {
1805
+ try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
1806
+ }
1807
+ }
1808
+
1761
1809
  // ─── Release: Sync Versions ─────────────────────────────────────────────────
1762
1810
 
1763
1811
  function syncVersions() {
@@ -1860,6 +1908,9 @@ export async function main(argv = process.argv.slice(2)) {
1860
1908
  case 'update':
1861
1909
  await manualUpdate();
1862
1910
  break;
1911
+ case 'repair':
1912
+ await repair();
1913
+ break;
1863
1914
  case 'release':
1864
1915
  syncVersions();
1865
1916
  if (!flags.has('--no-lock')) regenerateLockfile();
@@ -1889,6 +1940,7 @@ Usage:
1889
1940
  node install.mjs cleanup Remove stale temp/staging files (use --dry-run to preview)
1890
1941
  node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
1891
1942
  node install.mjs self-update Check for and install updates
1943
+ node install.mjs repair Recover a broken install: download latest tarball, re-run install
1892
1944
  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
1945
 
1894
1946
  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.2",
3
+ "version": "2.84.1",
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,123 @@
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
+ // Last-resort recovery string for users whose `claude-mem-lite repair` path
36
+ // itself failed (install.mjs missing / repair errored / retry still drifting).
37
+ // Duplicated in install.mjs::repair() catch; both are reachable when local
38
+ // scripts are broken, so neither can import a shared constant.
39
+ const TARBALL_FALLBACK =
40
+ 'T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install';
41
+
42
+ const [, , entryArg, ...rest] = process.argv;
43
+ if (!entryArg) {
44
+ process.stderr.write('[claude-mem-lite] hook-launcher: missing entry argument\n');
45
+ process.exit(1);
46
+ }
47
+
48
+ const entryAbs = entryArg.startsWith('/') ? entryArg : join(INSTALL_DIR, entryArg);
49
+
50
+ async function runEntry({ bustCache = false } = {}) {
51
+ // Mirror direct invocation: process.argv[1] is the entry, [2..] are its args.
52
+ process.argv = [process.argv[0], entryAbs, ...rest];
53
+ // Node ESM caches resolution outcomes (success AND failure) by URL. On the
54
+ // post-self-heal retry the freshly-written module file lives at the same
55
+ // path the first import already cached as ERR_MODULE_NOT_FOUND — without a
56
+ // cache-buster query the second await import() returns the cached rejection
57
+ // and the heal looks like it did nothing.
58
+ const url = pathToFileURL(entryAbs).href + (bustCache ? `?t=${Date.now()}` : '');
59
+ await import(url);
60
+ }
61
+
62
+ function isLocalModuleErr(e) {
63
+ if (!e || e.code !== 'ERR_MODULE_NOT_FOUND') return false;
64
+ const where = String(e.url || e.message || '');
65
+ return where.includes('.claude-mem-lite') || where.startsWith(`file://${INSTALL_DIR}`);
66
+ }
67
+
68
+ function recentHealAttempt() {
69
+ try {
70
+ return Date.now() - statSync(HEAL_MARKER).mtimeMs < HEAL_COOLDOWN_MS;
71
+ } catch { return false; }
72
+ }
73
+
74
+ function recordHealAttempt() {
75
+ try {
76
+ mkdirSync(RUNTIME_DIR, { recursive: true });
77
+ writeFileSync(HEAL_MARKER, String(Date.now()));
78
+ } catch { /* best-effort */ }
79
+ }
80
+
81
+ async function attemptHeal(reason) {
82
+ if (recentHealAttempt()) {
83
+ process.stderr.write(
84
+ `[claude-mem-lite] Self-heal skipped (last attempt < 6h ago).\n` +
85
+ `[claude-mem-lite] Manual recovery: claude-mem-lite repair\n` +
86
+ `[claude-mem-lite] If that fails, run: ${TARBALL_FALLBACK}\n`,
87
+ );
88
+ return false;
89
+ }
90
+ recordHealAttempt();
91
+ process.stderr.write(`[claude-mem-lite] Detected broken install (${reason}) — running self-heal\n`);
92
+ const installer = join(INSTALL_DIR, 'install.mjs');
93
+ if (!existsSync(installer)) {
94
+ process.stderr.write(
95
+ `[claude-mem-lite] install.mjs missing at ${installer} — cannot self-heal\n` +
96
+ `[claude-mem-lite] Manual recovery: ${TARBALL_FALLBACK}\n`,
97
+ );
98
+ return false;
99
+ }
100
+ const result = spawnSync(process.execPath, [installer, 'repair'], {
101
+ stdio: 'inherit',
102
+ timeout: 300000,
103
+ });
104
+ return result.status === 0;
105
+ }
106
+
107
+ try {
108
+ await runEntry();
109
+ } catch (e) {
110
+ if (!isLocalModuleErr(e)) throw e;
111
+ const reason = String(e.url || e.message).split('/').slice(-2).join('/');
112
+ const healed = await attemptHeal(reason);
113
+ if (!healed) throw e;
114
+ try {
115
+ await runEntry({ bustCache: true });
116
+ } catch (retryErr) {
117
+ process.stderr.write(
118
+ `[claude-mem-lite] Hook still failing after self-heal: ${retryErr.message}\n` +
119
+ `[claude-mem-lite] Manual recovery: ${TARBALL_FALLBACK}\n`,
120
+ );
121
+ process.exit(1);
122
+ }
123
+ }
@@ -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
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
  ];