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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli.mjs +1 -1
- package/hook-update.mjs +50 -12
- package/hooks/hooks.json +7 -7
- package/install.mjs +57 -10
- package/package.json +2 -1
- package/scripts/hook-launcher.mjs +108 -0
- package/scripts/post-tool-use.sh +3 -2
- package/scripts/pre-tool-recall.js +7 -0
- package/source-files.mjs +5 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
596
|
+
command: nodeHook('scripts/user-prompt-search.js'),
|
|
593
597
|
timeout: 2
|
|
594
598
|
},
|
|
595
599
|
{
|
|
596
600
|
type: 'command',
|
|
597
|
-
command:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
+
}
|
package/scripts/post-tool-use.sh
CHANGED
|
@@ -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
|
];
|