claude-mem-lite 3.57.0 → 3.58.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/adopt-cli.mjs +13 -7
- package/claudemd.mjs +14 -0
- package/hook-shared.mjs +4 -2
- package/hook-update.mjs +26 -0
- package/install.mjs +1 -1
- package/mem-cli.mjs +5 -3
- package/package.json +1 -1
- package/schema.mjs +43 -0
- package/scripts/launch.mjs +29 -3
- package/scripts/setup.sh +89 -7
- package/server.mjs +15 -26
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.58.0",
|
|
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.
|
|
3
|
+
"version": "3.58.0",
|
|
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/adopt-cli.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './memdir.mjs';
|
|
23
23
|
import {
|
|
24
24
|
writeManaged, removeManaged, isAdopted as claudeMdIsAdopted,
|
|
25
|
+
hasResidue as claudeMdHasResidue,
|
|
25
26
|
needsRefresh, migrateLegacyMemoryDir, hasLegacyMemdirSentinel,
|
|
26
27
|
claudeMdPath, detailDocPath,
|
|
27
28
|
} from './claudemd.mjs';
|
|
@@ -311,8 +312,8 @@ function statusAll() {
|
|
|
311
312
|
|
|
312
313
|
const known = listKnownProjectDirs();
|
|
313
314
|
let adoptedCount = 0;
|
|
314
|
-
for (const dir of known) if (
|
|
315
|
-
log(`[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block.`);
|
|
315
|
+
for (const dir of known) if (claudeMdHasResidue(dir, PLUGIN_SLUG)) adoptedCount++;
|
|
316
|
+
log(`[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block or partial residue (detail doc/state).`);
|
|
316
317
|
if (adoptedCount > 0) log('[adopt --status] run `claude-mem-lite unadopt --all` to remove every CLAUDE.md block.');
|
|
317
318
|
|
|
318
319
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
|
|
@@ -347,16 +348,20 @@ function unadoptAll(args) {
|
|
|
347
348
|
|
|
348
349
|
// 1. New scheme: scrub CLAUDE.md managed blocks across known project paths.
|
|
349
350
|
const projectDirs = listKnownProjectDirs();
|
|
350
|
-
let blocks = 0;
|
|
351
|
+
let blocks = 0, partial = 0;
|
|
351
352
|
for (const dir of projectDirs) {
|
|
352
|
-
|
|
353
|
+
// hasResidue, not isAdopted: the sweep must also catch PARTIAL residue
|
|
354
|
+
// (block without detail doc, or an orphaned doc/state sidecar) —
|
|
355
|
+
// isAdopted's block-AND-doc gate skipped those projects forever.
|
|
356
|
+
if (!claudeMdHasResidue(dir, PLUGIN_SLUG)) continue;
|
|
353
357
|
if (dryRun) {
|
|
354
|
-
log(`[unadopt --all --dry-run] ${dir} → would-remove CLAUDE.md block
|
|
358
|
+
log(`[unadopt --all --dry-run] ${dir} → would-remove plugin residue (CLAUDE.md block and/or detail doc/state)`);
|
|
355
359
|
blocks++;
|
|
356
360
|
continue;
|
|
357
361
|
}
|
|
358
362
|
const r = removeManaged(dir, PLUGIN_SLUG);
|
|
359
363
|
if (r.action === 'removed') { log(`[unadopt --all] ${dir} → removed`); blocks++; }
|
|
364
|
+
else { log(`[unadopt --all] ${dir} → cleaned partial residue (detail doc/state, no block)`); partial++; }
|
|
360
365
|
}
|
|
361
366
|
|
|
362
367
|
// 2. Legacy memory-dir cleanup across every memdir (foreign-content guarded).
|
|
@@ -372,7 +377,8 @@ function unadoptAll(args) {
|
|
|
372
377
|
}
|
|
373
378
|
|
|
374
379
|
log('');
|
|
375
|
-
|
|
380
|
+
const partialNote = partial > 0 ? ` (+${partial} partial-residue cleanup(s))` : '';
|
|
381
|
+
log(`[unadopt --all] ${dryRun ? 'would remove' : 'removed'} ${blocks} CLAUDE.md block(s)${partialNote} across ${projectDirs.length} known project(s); ${legacy} legacy memory-dir sentinel(s) ${dryRun ? 'pending' : 'cleaned'}.`);
|
|
376
382
|
if (projectDirs.length === 0) {
|
|
377
383
|
log('[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run `claude-mem-lite unadopt` from inside it.');
|
|
378
384
|
}
|
|
@@ -389,7 +395,7 @@ export function cmdUnadopt(args = []) {
|
|
|
389
395
|
|
|
390
396
|
const cwd = detectCwd();
|
|
391
397
|
if (dryRun) {
|
|
392
|
-
const blockState =
|
|
398
|
+
const blockState = claudeMdHasResidue(cwd, PLUGIN_SLUG) ? 'would-remove CLAUDE.md block + detail doc' : 'no CLAUDE.md block';
|
|
393
399
|
const legacy = hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG) ? 'would-clean legacy memory-dir sentinel' : 'no legacy residue';
|
|
394
400
|
log(`[unadopt --dry-run] ${cwd}`);
|
|
395
401
|
log(` ${blockState}`);
|
package/claudemd.mjs
CHANGED
|
@@ -98,6 +98,20 @@ export function isAdopted(cwd, slug) {
|
|
|
98
98
|
return blk.body !== null && existsSync(detailDocPath(cwd, slug));
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Any trace of adoption that unadopt should clean: managed block OR detail doc
|
|
103
|
+
* OR state sidecar. Deliberately weaker than isAdopted (whose AND lets a
|
|
104
|
+
* half-written adopt self-heal on the next SessionStart): the unadopt sweep
|
|
105
|
+
* gated on isAdopted skipped partial residue forever — e.g. a user deleted the
|
|
106
|
+
* detail doc but the CLAUDE.md block remained, and `unadopt --all` never
|
|
107
|
+
* removed it. removeManaged cleans all three pieces, so sweep on any of them.
|
|
108
|
+
*/
|
|
109
|
+
export function hasResidue(cwd, slug) {
|
|
110
|
+
return readBlock(cwd, slug).body !== null
|
|
111
|
+
|| existsSync(detailDocPath(cwd, slug))
|
|
112
|
+
|| existsSync(stateFilePath(cwd, slug));
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
/**
|
|
102
116
|
* Whether the installed block/doc has drifted from the shipped content — i.e.
|
|
103
117
|
* a version bump or a template edit means we should refresh. Returns true when
|
package/hook-shared.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID } from 'crypto';
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync, statSync, unlinkSync, chmodSync } from 'fs';
|
|
8
8
|
import { inferProject, debugCatch } from './utils.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { ensureDbWithWalRecovery, DB_DIR } from './schema.mjs';
|
|
10
10
|
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI, detectMode as detectLLMMode, callHaiku } from './haiku-client.mjs';
|
|
11
11
|
// Phase D: invited-memory sentinel detection. memdir.mjs/claudemd.mjs only pull in
|
|
12
12
|
// fs/path/os/crypto; adopt-content.mjs is pure strings. No circular deps —
|
|
@@ -154,7 +154,9 @@ export function createSessionId() {
|
|
|
154
154
|
|
|
155
155
|
export function openDb() {
|
|
156
156
|
try {
|
|
157
|
-
|
|
157
|
+
// WAL-corruption self-heal (was server.mjs-only): without it, hooks stayed
|
|
158
|
+
// silently dead (null DB) on a corrupt WAL until the next MCP server start.
|
|
159
|
+
return ensureDbWithWalRecovery();
|
|
158
160
|
} catch {
|
|
159
161
|
return null;
|
|
160
162
|
}
|
package/hook-update.mjs
CHANGED
|
@@ -635,6 +635,32 @@ function smokeInstalledRelease(targetDir) {
|
|
|
635
635
|
const p = join(targetDir, entry);
|
|
636
636
|
if (existsSync(p)) execSync(`${q(process.execPath)} --check ${q(p)}`, { timeout: 10000, stdio: 'ignore' });
|
|
637
637
|
}
|
|
638
|
+
// `cli.mjs help` exits without opening the DB, so it cannot see a
|
|
639
|
+
// present-but-unusable better-sqlite3 binding: npm >= 12 blocks
|
|
640
|
+
// install/lifecycle scripts by default, so the staging `npm install`
|
|
641
|
+
// above exits 0 with the native .node never compiled (a Node major bump
|
|
642
|
+
// strands a stale ABI the same way). Direct installs register server.mjs
|
|
643
|
+
// without the launch.mjs probe, so this gate is their only check. Probe
|
|
644
|
+
// in a child process (execSync so the unit-test mock intercepts, and so
|
|
645
|
+
// the running old-version process's require cache can't mask it); on
|
|
646
|
+
// failure rebuild with scripts enabled for just this dep — plain-rebuild
|
|
647
|
+
// fallback for older npm — then re-probe. A still-broken binding throws
|
|
648
|
+
// out of the try, smoke fails, and the caller rolls back to the old
|
|
649
|
+
// (working) install.
|
|
650
|
+
if (existsSync(join(targetDir, 'node_modules', 'better-sqlite3'))) {
|
|
651
|
+
const probeSrc = 'const{createRequire}=require("node:module");const D=createRequire(process.argv[1])("better-sqlite3");new D(":memory:").close();';
|
|
652
|
+
const probeCmd = `${q(process.execPath)} -e ${q(probeSrc)} ${q(join(targetDir, 'package.json'))}`;
|
|
653
|
+
try {
|
|
654
|
+
execSync(probeCmd, { timeout: 20000, stdio: 'ignore' });
|
|
655
|
+
} catch {
|
|
656
|
+
try {
|
|
657
|
+
execSync('npm rebuild better-sqlite3 --dangerously-allow-all-scripts', { cwd: targetDir, timeout: 120000, stdio: 'ignore' });
|
|
658
|
+
} catch {
|
|
659
|
+
execSync('npm rebuild better-sqlite3', { cwd: targetDir, timeout: 120000, stdio: 'ignore' });
|
|
660
|
+
}
|
|
661
|
+
execSync(probeCmd, { timeout: 20000, stdio: 'ignore' });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
638
664
|
return true;
|
|
639
665
|
} catch (e) {
|
|
640
666
|
debugLog('WARN', 'hook-update', `post-install smoke failed (rolling back): ${e.message}`);
|
package/install.mjs
CHANGED
|
@@ -466,7 +466,7 @@ if (IS_DEV) {
|
|
|
466
466
|
ok(`better-sqlite3: ${verify.action}`);
|
|
467
467
|
} else {
|
|
468
468
|
fail(`better-sqlite3 binding unusable after rebuild: ${verify.error}`);
|
|
469
|
-
log('Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --
|
|
469
|
+
log('Try manually: cd ' + INSTALL_DIR + ' && npm rebuild better-sqlite3 --dangerously-allow-all-scripts');
|
|
470
470
|
process.exit(1);
|
|
471
471
|
}
|
|
472
472
|
}
|
package/mem-cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// No MCP SDK or heavy deps — only imports schema.mjs and utils.mjs
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
-
import {
|
|
6
|
+
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
7
|
import { truncate, typeIcon, inferProject, scrubSecrets } from './utils.mjs';
|
|
8
8
|
import { resolveProject } from './project-utils.mjs';
|
|
9
9
|
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
@@ -3156,14 +3156,16 @@ export async function run(argv) {
|
|
|
3156
3156
|
}
|
|
3157
3157
|
|
|
3158
3158
|
// adopt / unadopt do pure filesystem work on ~/.claude/projects/<encoded>/memory/ —
|
|
3159
|
-
// no DB needed. Route them before
|
|
3159
|
+
// no DB needed. Route them before the DB open so an unbootable DB doesn't block.
|
|
3160
3160
|
if (cmd === 'adopt') { cmdAdopt(cmdArgs); return; }
|
|
3161
3161
|
if (cmd === 'unadopt') { cmdUnadopt(cmdArgs); return; }
|
|
3162
3162
|
if (cmd === 'memdir-audit') { cmdMemdirAudit(cmdArgs); return; }
|
|
3163
3163
|
|
|
3164
3164
|
let db;
|
|
3165
3165
|
try {
|
|
3166
|
-
|
|
3166
|
+
// Corruption-gated WAL recovery (shared with server/hooks) — a corrupt WAL
|
|
3167
|
+
// previously threw here with no auto-repair until the next MCP start.
|
|
3168
|
+
db = ensureDbWithWalRecovery({ warn: (m) => process.stderr.write(`[mem] ${m}\n`) });
|
|
3167
3169
|
} catch (e) {
|
|
3168
3170
|
out(`[mem] Error: Cannot open database: ${e.message}`);
|
|
3169
3171
|
out(`[mem] DB path: ${DB_PATH}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.58.0",
|
|
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",
|
package/schema.mjs
CHANGED
|
@@ -1000,6 +1000,49 @@ export function ensureDb() {
|
|
|
1000
1000
|
}
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Whether an open/init error carries a genuine corruption signature. WAL-delete
|
|
1005
|
+
* recovery is ONLY safe for these: on a transient error (SQLITE_BUSY) or the
|
|
1006
|
+
* forward-version guard throw, deleting the WAL would discard committed-but-
|
|
1007
|
+
* uncheckpointed transactions — silent data loss.
|
|
1008
|
+
*/
|
|
1009
|
+
export function isDbCorruptionError(err) {
|
|
1010
|
+
return /SQLITE_CORRUPT|SQLITE_NOTADB|malformed|not a database|disk image/i
|
|
1011
|
+
.test(`${err?.code || ''} ${err?.message || ''}`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* ensureDb with corruption-gated WAL recovery. Was inlined in server.mjs only,
|
|
1016
|
+
* so hooks (openDb → silent null) and the CLI (raw throw) stayed degraded on a
|
|
1017
|
+
* corrupt WAL until the next MCP server start. One shared implementation now
|
|
1018
|
+
* serves all three openers.
|
|
1019
|
+
*
|
|
1020
|
+
* Non-corruption failure → rethrows the original error, WAL/SHM left intact.
|
|
1021
|
+
* Corruption → deletes -wal/-shm, retries once; a still-failing retry rethrows
|
|
1022
|
+
* with `err.walRecoveryAttempted = true` so callers can word their fatal hint
|
|
1023
|
+
* accurately (recovery already tried vs WAL deliberately preserved).
|
|
1024
|
+
*
|
|
1025
|
+
* @param {{warn?: (msg: string) => void, info?: (msg: string) => void}} [opts]
|
|
1026
|
+
*/
|
|
1027
|
+
export function ensureDbWithWalRecovery({ warn, info } = {}) {
|
|
1028
|
+
try {
|
|
1029
|
+
return ensureDb();
|
|
1030
|
+
} catch (firstErr) {
|
|
1031
|
+
if (!isDbCorruptionError(firstErr)) throw firstErr;
|
|
1032
|
+
warn?.(`DB corruption detected, attempting WAL recovery: ${firstErr.message}`);
|
|
1033
|
+
try { rmSync(DB_PATH + '-wal', { force: true }); } catch { /* best-effort */ }
|
|
1034
|
+
try { rmSync(DB_PATH + '-shm', { force: true }); } catch { /* best-effort */ }
|
|
1035
|
+
try {
|
|
1036
|
+
const db = ensureDb();
|
|
1037
|
+
info?.('DB recovered after WAL cleanup');
|
|
1038
|
+
return db;
|
|
1039
|
+
} catch (retryErr) {
|
|
1040
|
+
try { retryErr.walRecoveryAttempted = true; } catch { /* frozen error — fine */ }
|
|
1041
|
+
throw retryErr;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1003
1046
|
/**
|
|
1004
1047
|
* Create FTS5 virtual table + sync triggers for a content table.
|
|
1005
1048
|
* Idempotent: skips if already exists. Exported for test helpers.
|
package/scripts/launch.mjs
CHANGED
|
@@ -36,11 +36,37 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
|
36
36
|
// intact but the .node binary stale → server FATALs with "Could not locate
|
|
37
37
|
// the bindings file" on first DB open. Probe + auto-rebuild before launching.
|
|
38
38
|
try {
|
|
39
|
-
const { ensureBetterSqlite3Working } = await import('../lib/binding-probe.mjs');
|
|
40
|
-
|
|
39
|
+
const { ensureBetterSqlite3Working, probeBetterSqlite3Binding } = await import('../lib/binding-probe.mjs');
|
|
40
|
+
// The rebuild inside ensureBetterSqlite3Working mutates node_modules — the
|
|
41
|
+
// same write class as install/repair/update, and this was the ONE rebuild
|
|
42
|
+
// path outside the shared install.lock: a second MCP launch or a concurrent
|
|
43
|
+
// `install.mjs repair` (hook-launcher heal) could clobber the .node
|
|
44
|
+
// mid-compile. Take the lock for the rebuild-capable path; a live peer →
|
|
45
|
+
// wait up to 10s, then degrade to a probe-only pass (healthy binding
|
|
46
|
+
// proceeds; a broken one defers to the peer instead of racing it).
|
|
47
|
+
const { acquireLock } = await import('../lib/proc-lock.mjs');
|
|
48
|
+
const { resolveDataDir } = await import('../lib/resolve-data-dir.mjs');
|
|
49
|
+
const lockPath = join(resolveDataDir(process.env.CLAUDE_MEM_DIR), 'runtime', 'install.lock');
|
|
50
|
+
let release = null;
|
|
51
|
+
for (let i = 0; i < 20 && !(release = acquireLock(lockPath)); i++) {
|
|
52
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
53
|
+
}
|
|
54
|
+
let verify;
|
|
55
|
+
try {
|
|
56
|
+
if (release) {
|
|
57
|
+
verify = await ensureBetterSqlite3Working(ROOT);
|
|
58
|
+
} else {
|
|
59
|
+
const probe = await probeBetterSqlite3Binding(ROOT);
|
|
60
|
+
verify = probe.ok
|
|
61
|
+
? { ok: true, action: 'verified' }
|
|
62
|
+
: { ok: false, error: `${probe.error} (another install/repair holds the lock — not rebuilding concurrently; reconnect with /mcp once it finishes)` };
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
if (release) release();
|
|
66
|
+
}
|
|
41
67
|
if (!verify.ok) {
|
|
42
68
|
process.stderr.write(`[claude-mem-lite] better-sqlite3 binding unusable: ${verify.error}\n`);
|
|
43
|
-
process.stderr.write(`[claude-mem-lite] Repair: cd "${ROOT}" && npm rebuild better-sqlite3 --
|
|
69
|
+
process.stderr.write(`[claude-mem-lite] Repair: cd "${ROOT}" && npm rebuild better-sqlite3 --dangerously-allow-all-scripts\n`);
|
|
44
70
|
process.exit(1);
|
|
45
71
|
}
|
|
46
72
|
if (verify.action === 'rebuilt') {
|
package/scripts/setup.sh
CHANGED
|
@@ -80,20 +80,22 @@ mkdir -p "$DATA_DIR/runtime" 2>/dev/null || true
|
|
|
80
80
|
|
|
81
81
|
mark_deps_broken() {
|
|
82
82
|
local reason="$1"
|
|
83
|
+
local repair="${2:-npm install --omit=dev}"
|
|
83
84
|
# Embed reason + repair command so hook.mjs renders a complete error without
|
|
84
85
|
# having to re-derive them. Delegate JSON serialization to node so embedded
|
|
85
86
|
# quotes / shell metachars in $ROOT or $reason can't produce an invalid file
|
|
86
87
|
# (bash `printf '"..%s.."'` cannot escape arbitrary strings safely; v2.79.1 fix).
|
|
87
88
|
# shellcheck disable=SC2016 # node script single-quoted on purpose; vars passed via env (MARK_*), not shell expansion
|
|
88
|
-
MARK_REASON="$reason" MARK_ROOT="$ROOT" MARK_FLAG="$DEPS_FLAG" node -e '
|
|
89
|
+
MARK_REASON="$reason" MARK_ROOT="$ROOT" MARK_FLAG="$DEPS_FLAG" MARK_REPAIR="$repair" node -e '
|
|
89
90
|
const fs = require("fs");
|
|
90
91
|
const reason = process.env.MARK_REASON || "unknown";
|
|
91
92
|
const root = process.env.MARK_ROOT || "";
|
|
93
|
+
const repair = process.env.MARK_REPAIR || "npm install --omit=dev";
|
|
92
94
|
fs.writeFileSync(process.env.MARK_FLAG, JSON.stringify({
|
|
93
95
|
ts: new Date().toISOString(),
|
|
94
96
|
reason,
|
|
95
97
|
root,
|
|
96
|
-
repair: `cd ${JSON.stringify(root)} &&
|
|
98
|
+
repair: `cd ${JSON.stringify(root)} && ${repair}`,
|
|
97
99
|
}) + "\n");
|
|
98
100
|
' 2>/dev/null || true
|
|
99
101
|
}
|
|
@@ -107,7 +109,6 @@ if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
|
107
109
|
if [[ -d "$DATA_DIR/node_modules/better-sqlite3" ]]; then
|
|
108
110
|
if ln -sfn "$DATA_DIR/node_modules" "$ROOT/node_modules" 2>/dev/null; then
|
|
109
111
|
log_ok "Dependencies linked from $DATA_DIR"
|
|
110
|
-
mark_deps_ok
|
|
111
112
|
fi
|
|
112
113
|
fi
|
|
113
114
|
# Slow path: npm install (first-time only, ~10-20s for native addon)
|
|
@@ -115,15 +116,96 @@ if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
|
115
116
|
log_info "Installing dependencies (first-time setup)..."
|
|
116
117
|
if (cd "$ROOT" && npm install --omit=dev --no-audit --no-fund 2>&1) >&2; then
|
|
117
118
|
log_ok "Dependencies installed"
|
|
118
|
-
mark_deps_ok
|
|
119
119
|
else
|
|
120
120
|
log_warn "Dependency install failed — hooks may have limited functionality (flag: $DEPS_FLAG)"
|
|
121
121
|
mark_deps_broken "npm install --omit=dev failed in plugin cache root"
|
|
122
122
|
fi
|
|
123
123
|
fi
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# 6b. Binding probe: node_modules/better-sqlite3 PRESENT is not node binding
|
|
127
|
+
# WORKING. npm >= 12 blocks install/lifecycle scripts by default, so the
|
|
128
|
+
# `npm install` above exits 0 with the native .node binding never compiled
|
|
129
|
+
# (and a Node major upgrade strands a stale-ABI binding the same way) —
|
|
130
|
+
# pre-v3.58 this branch called mark_deps_ok on directory presence alone,
|
|
131
|
+
# leaving every hook dead with a false-green flag until the MCP server's
|
|
132
|
+
# own probe ran. Probe via the shared fix point lib/binding-probe.mjs
|
|
133
|
+
# (opens a :memory: DB; auto-rebuilds with --dangerously-allow-all-scripts,
|
|
134
|
+
# plain-rebuild fallback for older npm). The ABI-keyed marker lives inside
|
|
135
|
+
# node_modules/ — WITH the tree it certifies — so healthy sessions cost one
|
|
136
|
+
# stat, a new plugin-cache version dir (fresh node_modules) re-probes, and
|
|
137
|
+
# a Node upgrade (new ABI) re-probes. While broken, every SessionStart
|
|
138
|
+
# retries the rebuild until it heals.
|
|
139
|
+
if [[ -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
140
|
+
NODE_ABI="$(node -p 'process.versions.modules' 2>/dev/null || echo 0)"
|
|
141
|
+
BINDING_MARKER="$ROOT/node_modules/.mem-binding-ok-$NODE_ABI"
|
|
142
|
+
if [[ -f "$BINDING_MARKER" ]]; then
|
|
143
|
+
mark_deps_ok
|
|
144
|
+
# shellcheck disable=SC2016 # node script single-quoted on purpose; ROOT passed via env, not shell expansion
|
|
145
|
+
elif PROBE_ROOT="$ROOT" node --input-type=module -e '
|
|
146
|
+
// NOTE: this whole script sits in a single-quoted bash string — no
|
|
147
|
+
// apostrophes anywhere in it.
|
|
148
|
+
const { pathToFileURL } = await import("node:url");
|
|
149
|
+
const { join } = await import("node:path");
|
|
150
|
+
const root = process.env.PROBE_ROOT;
|
|
151
|
+
const libUrl = (f) => pathToFileURL(join(root, "lib", f)).href;
|
|
152
|
+
let helpers = null;
|
|
153
|
+
try {
|
|
154
|
+
const [probeMod, lockMod, dirMod] = await Promise.all(
|
|
155
|
+
["binding-probe.mjs", "proc-lock.mjs", "resolve-data-dir.mjs"].map((f) => import(libUrl(f))));
|
|
156
|
+
helpers = { ...probeMod, ...lockMod, ...dirMod };
|
|
157
|
+
} catch {
|
|
158
|
+
// Probe helpers missing (half-installed tree) — fall back to a bare
|
|
159
|
+
// probe with no rebuild: a WORKING binding must still clear the flag,
|
|
160
|
+
// and a helperless broken tree is repaired by the hook-launcher path.
|
|
161
|
+
const { createRequire } = await import("node:module");
|
|
162
|
+
try {
|
|
163
|
+
const D = createRequire(join(root, "package.json"))("better-sqlite3");
|
|
164
|
+
new D(":memory:").close();
|
|
165
|
+
process.exit(0);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
process.stderr.write(`[claude-mem-lite] binding probe: ${e.message}\n`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Probe first — read-only, no lock needed. Healthy binding exits here.
|
|
172
|
+
const first = await helpers.probeBetterSqlite3Binding(root);
|
|
173
|
+
if (first.ok) process.exit(0);
|
|
174
|
+
// Broken: rebuild ONLY under the shared install.lock (a second MCP launch
|
|
175
|
+
// or install.mjs repair rebuilding the same node_modules concurrently can
|
|
176
|
+
// tear the .node), and with the exec bounded to 20s — this script runs
|
|
177
|
+
// under the SessionStart hook cap (hooks.json timeout 30), and letting the
|
|
178
|
+
// hook SIGKILL a mid-flight node-gyp leaves a partial .node with no flag
|
|
179
|
+
// written. On lock-miss or timeout: mark broken and defer the heal to the
|
|
180
|
+
// MCP launch path (no hook cap, same lock).
|
|
181
|
+
const lockPath = join(helpers.resolveDataDir(process.env.CLAUDE_MEM_DIR), "runtime", "install.lock");
|
|
182
|
+
const release = helpers.acquireLock(lockPath);
|
|
183
|
+
if (!release) {
|
|
184
|
+
const firstLine = String(first.error).split("\n")[0];
|
|
185
|
+
process.stderr.write(`[claude-mem-lite] binding probe: ${firstLine} (another install/repair in flight — deferring heal)\n`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
let r;
|
|
189
|
+
try {
|
|
190
|
+
const { execSync } = await import("node:child_process");
|
|
191
|
+
r = await helpers.ensureBetterSqlite3Working(root, {
|
|
192
|
+
exec: (cmd, opts) => execSync(cmd, { ...opts, timeout: 20000 }),
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
// process.exit skips finally blocks — exits live BELOW this so the
|
|
196
|
+
// lock is always released.
|
|
197
|
+
release();
|
|
198
|
+
}
|
|
199
|
+
if (!r.ok) { process.stderr.write(`[claude-mem-lite] binding probe: ${r.error}\n`); process.exit(1); }
|
|
200
|
+
if (r.action === "rebuilt") process.stderr.write("[claude-mem-lite] rebuilt better-sqlite3 binding for current Node ABI\n");
|
|
201
|
+
'; then
|
|
202
|
+
rm -f "$ROOT/node_modules/.mem-binding-ok-"* 2>/dev/null || true
|
|
203
|
+
touch "$BINDING_MARKER" 2>/dev/null || true
|
|
204
|
+
mark_deps_ok
|
|
205
|
+
else
|
|
206
|
+
log_warn "better-sqlite3 native binding unusable — hooks degraded until repaired (flag: $DEPS_FLAG)"
|
|
207
|
+
mark_deps_broken "better-sqlite3 binding probe/rebuild failed (npm >= 12 blocks compile scripts by default)" "npm rebuild better-sqlite3 --dangerously-allow-all-scripts"
|
|
208
|
+
fi
|
|
127
209
|
fi
|
|
128
210
|
|
|
129
211
|
# 7. MCP cleanup: one-shot purge of stale global MCP registrations.
|
package/server.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { truncate, typeIcon, inferProject, scrubSecrets, fmtDate, debugLog, debugCatch, isPathConfined } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
11
11
|
import { reRankWithContext, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
@@ -66,36 +66,25 @@ const { version: PKG_VERSION } = require('./package.json');
|
|
|
66
66
|
|
|
67
67
|
// ─── Database ───────────────────────────────────────────────────────────────
|
|
68
68
|
|
|
69
|
-
import {
|
|
69
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, chmodSync } from 'fs';
|
|
70
70
|
|
|
71
71
|
let db;
|
|
72
72
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.error(`[claude-mem-lite] Left WAL/SHM intact (not a corruption error). If this persists, retry or reinstall: node install.mjs install`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
// Recovery: remove WAL/SHM files (corrupt WAL is the most common cause) and retry
|
|
87
|
-
debugLog('WARN', 'server', `DB corruption detected, attempting WAL recovery: ${firstErr.message}`);
|
|
88
|
-
try { rmSync(DB_PATH + '-wal', { force: true }); } catch {}
|
|
89
|
-
try { rmSync(DB_PATH + '-shm', { force: true }); } catch {}
|
|
90
|
-
try {
|
|
91
|
-
db = ensureDb();
|
|
92
|
-
debugLog('INFO', 'server', 'DB recovered after WAL cleanup');
|
|
93
|
-
} catch (retryErr) {
|
|
94
|
-
// Fatal: log and exit with descriptive message (Claude Code shows stderr)
|
|
95
|
-
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${retryErr.message}`);
|
|
73
|
+
// Corruption-gated WAL recovery lives in schema.mjs (shared with hooks/CLI
|
|
74
|
+
// since the D#8-adjacent P3 fix); the server keeps only its exit semantics.
|
|
75
|
+
db = ensureDbWithWalRecovery({
|
|
76
|
+
warn: (m) => debugLog('WARN', 'server', m),
|
|
77
|
+
info: (m) => debugLog('INFO', 'server', m),
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Fatal: log and exit with descriptive message (Claude Code shows stderr)
|
|
81
|
+
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
|
|
82
|
+
if (err.walRecoveryAttempted) {
|
|
96
83
|
console.error(`[claude-mem-lite] Try: rm "${DB_PATH}-wal" "${DB_PATH}-shm" or reinstall with: node install.mjs install`);
|
|
97
|
-
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`[claude-mem-lite] Left WAL/SHM intact (not a corruption error). If this persists, retry or reinstall: node install.mjs install`);
|
|
98
86
|
}
|
|
87
|
+
process.exit(1);
|
|
99
88
|
}
|
|
100
89
|
// Server process uses longer busy_timeout for concurrent MCP requests
|
|
101
90
|
db.pragma('busy_timeout = 5000');
|