claude-mem-lite 6.3.0 → 6.4.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 +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +22 -2
- package/README.zh-CN.md +19 -2
- package/hook-update.mjs +6 -1
- package/install.mjs +303 -34
- package/lib/data-paths.mjs +33 -0
- package/lib/db-backup.mjs +42 -17
- package/lib/plugin-key.mjs +4 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -1
- package/schema.mjs +9 -11
- package/scripts/hook-launcher.mjs +25 -4
- package/server.mjs +57 -1
- package/source-files.mjs +5 -0
- package/tool-schemas.mjs +15 -2
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"name": "sdsrss"
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
|
-
"description": "Plugins by sdsrss"
|
|
8
|
-
"homepage": "https://github.com/sdsrss/claude-mem-lite"
|
|
7
|
+
"description": "Plugins by sdsrss"
|
|
9
8
|
},
|
|
10
9
|
"plugins": [
|
|
11
10
|
{
|
|
12
11
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "6.
|
|
12
|
+
"version": "6.4.0",
|
|
14
13
|
"source": "./",
|
|
14
|
+
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
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
|
}
|
|
17
17
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.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/README.md
CHANGED
|
@@ -598,7 +598,12 @@ Your data directory (`~/.claude-mem-lite/`) is untouched by install/rollback; sc
|
|
|
598
598
|
|
|
599
599
|
### doctor
|
|
600
600
|
|
|
601
|
-
Checks Node.js version, dependencies, server/hook files, database integrity, FTS5 indexes,
|
|
601
|
+
Checks Node.js version, dependencies, server/hook files, database integrity, FTS5 indexes,
|
|
602
|
+
stale processes, MCP registration, and whether the marketplace clone can still be updated.
|
|
603
|
+
|
|
604
|
+
It runs `claude mcp list` to answer the registration question, and that **health-checks every
|
|
605
|
+
MCP server you have configured** — i.e. briefly launches each one, including remote endpoints.
|
|
606
|
+
`status` deliberately does not: on a plugin install it answers from the manifest instead.
|
|
602
607
|
|
|
603
608
|
### status
|
|
604
609
|
|
|
@@ -617,9 +622,11 @@ claude-mem-lite repair
|
|
|
617
622
|
**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:
|
|
618
623
|
|
|
619
624
|
```bash
|
|
620
|
-
T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/
|
|
625
|
+
T=$(mktemp -d) && U=$(curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/releases/latest | grep -o '"tarball_url"[^,]*' | cut -d'"' -f4) && curl -sL "$U" | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
|
|
621
626
|
```
|
|
622
627
|
|
|
628
|
+
It resolves the latest **release** tag first. A shell one-liner cannot verify the release signature the way `repair` does, so running it is a trust decision you are making explicitly — that is why it is the last resort and not the first suggestion.
|
|
629
|
+
|
|
623
630
|
After it finishes, `~/.claude-mem-lite/` is back in sync with the latest release and `claude-mem-lite repair` is available for next time.
|
|
624
631
|
|
|
625
632
|
## Uninstall
|
|
@@ -643,6 +650,19 @@ Data in `~/.claude-mem-lite/` is preserved by default. Delete manually if needed
|
|
|
643
650
|
rm -rf ~/.claude-mem-lite/
|
|
644
651
|
```
|
|
645
652
|
|
|
653
|
+
**`/plugin uninstall` does not delete the plugin cache.** Claude Code materializes each
|
|
654
|
+
version under `~/.claude/plugins/cache/`, with its own `node_modules`. While the plugin is
|
|
655
|
+
installed these get pruned to the newest three (SessionStart does it, and so does the update
|
|
656
|
+
path), so the directory is bounded — measured at 241 MB — not unbounded. But `/plugin
|
|
657
|
+
uninstall` removes the manifest and there is no uninstall hook a plugin can attach to, so the
|
|
658
|
+
hooks stop firing and whatever is left is never reclaimed. `claude-mem-lite uninstall` does
|
|
659
|
+
reclaim it, but after `/plugin uninstall` that command may no longer be on your PATH. Either
|
|
660
|
+
run it **first**, or delete the directory yourself:
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
rm -rf ~/.claude/plugins/cache/sdsrss/claude-mem-lite
|
|
664
|
+
```
|
|
665
|
+
|
|
646
666
|
### Mixed-install residue (read this if you've used multiple install methods)
|
|
647
667
|
|
|
648
668
|
`/plugin uninstall` only removes the plugin manifest — it **does not touch `~/.claude/settings.json`**. If you've ever run `claude-mem-lite install` (npx or git-clone path), hook entries pointing at `~/.claude-mem-lite/hook.mjs` were written into your user-global settings, and they keep firing after `/plugin uninstall`. If `~/.claude-mem-lite/hook.mjs` still exists they double-fire alongside the plugin; if you also ran `rm -rf ~/.claude-mem-lite/` they error every session.
|
package/README.zh-CN.md
CHANGED
|
@@ -478,7 +478,11 @@ npx claude-mem-lite doctor # 诊断问题
|
|
|
478
478
|
|
|
479
479
|
### doctor
|
|
480
480
|
|
|
481
|
-
检查 Node.js 版本、依赖、服务器/钩子文件、数据库完整性、FTS5
|
|
481
|
+
检查 Node.js 版本、依赖、服务器/钩子文件、数据库完整性、FTS5 索引、残留进程、MCP 注册状态,
|
|
482
|
+
以及 marketplace clone 是否还能被更新。
|
|
483
|
+
|
|
484
|
+
它用 `claude mcp list` 回答注册状态那一问,而这会**对你配置的每个 MCP server 做健康检查**——
|
|
485
|
+
也就是逐个短暂启动,包括远程 endpoint。`status` 刻意不这么做:插件形态下它从 manifest 回答。
|
|
482
486
|
|
|
483
487
|
### status
|
|
484
488
|
|
|
@@ -497,9 +501,11 @@ claude-mem-lite repair
|
|
|
497
501
|
**如果 `repair` 自己也跑不起来**(bin 比 v2.84.0 旧,或 bin 也坏了),用这条单行命令——它把最新 tarball 拉到临时目录、跑 *那份* tarball 里的 `install.mjs`,完全不依赖你磁盘上的任何文件:
|
|
498
502
|
|
|
499
503
|
```bash
|
|
500
|
-
T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/
|
|
504
|
+
T=$(mktemp -d) && U=$(curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/releases/latest | grep -o '"tarball_url"[^,]*' | cut -d'"' -f4) && curl -sL "$U" | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
|
|
501
505
|
```
|
|
502
506
|
|
|
507
|
+
它会先解析出最新 **release** 的 tag。shell 单行命令无法像 `repair` 那样校验 release 签名,所以跑它等于你自己做了一次信任决定——这也是它排在最后、而不是被优先推荐的原因。
|
|
508
|
+
|
|
503
509
|
跑完之后,`~/.claude-mem-lite/` 就和最新 release 对齐,`claude-mem-lite repair` 下次再遇到类似问题也能直接用了。
|
|
504
510
|
|
|
505
511
|
## 卸载
|
|
@@ -523,6 +529,17 @@ npx claude-mem-lite uninstall --purge
|
|
|
523
529
|
rm -rf ~/.claude-mem-lite/
|
|
524
530
|
```
|
|
525
531
|
|
|
532
|
+
**`/plugin uninstall` 不会删 plugin cache。** Claude Code 会把每个版本展开到
|
|
533
|
+
`~/.claude/plugins/cache/` 下,各带一份 `node_modules`。插件还装着的时候,这些会被裁剪到最新
|
|
534
|
+
三个版本(SessionStart 会做,更新路径也会做),所以这个目录是有上限的——实测 241 MB——而不是无限
|
|
535
|
+
增长。但 `/plugin uninstall` 只移除 manifest,插件又没有可挂的卸载生命周期钩子,于是钩子停止触发,
|
|
536
|
+
剩下的东西再也没人回收。`claude-mem-lite uninstall` 能回收它,但 `/plugin uninstall` 之后这个命令
|
|
537
|
+
可能已经不在 PATH 上了。所以要么**先**跑它,要么自己删:
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
rm -rf ~/.claude/plugins/cache/sdsrss/claude-mem-lite
|
|
541
|
+
```
|
|
542
|
+
|
|
526
543
|
### 混装残留(用过多种安装方式的话务必看一下)
|
|
527
544
|
|
|
528
545
|
`/plugin uninstall` 只删 plugin manifest,**不会动 `~/.claude/settings.json`**。如果你曾经跑过 `claude-mem-lite install`(npx 或 git-clone 路径),指向 `~/.claude-mem-lite/hook.mjs` 的 hook 条目就被写进了你的 user-global settings;`/plugin uninstall` 之后它们还在每会话触发。如果 `~/.claude-mem-lite/hook.mjs` 还在 → 与 plugin 双触发;如果你又 `rm -rf ~/.claude-mem-lite/` → 每次会话报错。
|
package/hook-update.mjs
CHANGED
|
@@ -20,7 +20,12 @@ import {
|
|
|
20
20
|
import { join, dirname, resolve } from 'node:path';
|
|
21
21
|
import { pathToFileURL } from 'node:url';
|
|
22
22
|
import { tmpdir, homedir } from 'node:os';
|
|
23
|
-
|
|
23
|
+
// lib/data-paths.mjs, NOT schema.mjs: this module is what install.mjs::repair() imports to
|
|
24
|
+
// reach the signature-verified release path, and schema.mjs statically imports
|
|
25
|
+
// better-sqlite3 — so importing two path constants from there made the verified repair
|
|
26
|
+
// unreachable on exactly the broken-install state it exists to repair (2026-09-08).
|
|
27
|
+
// tests/repair-path-no-native-dep.test.mjs fails on any package edge reachable from here.
|
|
28
|
+
import { DB_DIR, CODE_DIR } from './lib/data-paths.mjs';
|
|
24
29
|
import { debugCatch, debugLog } from './utils.mjs';
|
|
25
30
|
import { NATIVE_BINDING_SOURCE_BUILD_CMD } from './lib/binding-probe.mjs';
|
|
26
31
|
// Local manifest is fallback only — the active manifest is loaded from the
|
package/install.mjs
CHANGED
|
@@ -54,7 +54,7 @@ const SERVER_PATH = join(INSTALL_DIR, 'server.mjs');
|
|
|
54
54
|
const HOOK_PATH = join(INSTALL_DIR, 'hook.mjs');
|
|
55
55
|
// P2-7: both constants and the predicate come from lib/plugin-key.mjs, which hook.mjs also
|
|
56
56
|
// imports — this pair used to be typed out in each.
|
|
57
|
-
import { MARKETPLACE_KEY, PLUGIN_KEY, isPluginExplicitlyDisabled } from './lib/plugin-key.mjs';
|
|
57
|
+
import { MARKETPLACE_KEY, PLUGIN_KEY, PLUGIN_NAME, isPluginExplicitlyDisabled } from './lib/plugin-key.mjs';
|
|
58
58
|
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
59
59
|
|
|
60
60
|
import {
|
|
@@ -68,7 +68,9 @@ import {
|
|
|
68
68
|
probeBetterSqlite3Binding,
|
|
69
69
|
ensureBetterSqlite3Working,
|
|
70
70
|
nativeBindingRepairHint,
|
|
71
|
+
isNativeBindingError,
|
|
71
72
|
} from './lib/binding-probe.mjs';
|
|
73
|
+
import { readSnapshots } from './lib/db-backup.mjs';
|
|
72
74
|
import { detectInstallShape, probeRuntimeRoots } from './lib/install-shape.mjs';
|
|
73
75
|
import { probeSchemaCompat, schemaSkewRemedy } from './lib/schema-skew.mjs';
|
|
74
76
|
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
@@ -333,6 +335,147 @@ function isDevInstall() {
|
|
|
333
335
|
}
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
// Last-resort recovery command, printed when the signature-verified repair path itself
|
|
339
|
+
// fails. It resolves the latest RELEASE tarball via the GitHub API rather than fetching
|
|
340
|
+
// `/tarball`, which serves the DEFAULT BRANCH — unreleased WIP. That mattered: repair()
|
|
341
|
+
// exists because the old auto-path ran main HEAD unverified, and until 2026-09-08 the
|
|
342
|
+
// fallback it printed on failure handed the user exactly that behaviour back. A shell
|
|
343
|
+
// one-liner cannot check an Ed25519 signature, so this remains a trust decision the user
|
|
344
|
+
// makes explicitly; pinning it to a release at least removes the unreleased-WIP half.
|
|
345
|
+
//
|
|
346
|
+
// FOUR surfaces carry this string — here, scripts/hook-launcher.mjs (pure-`node:` charter,
|
|
347
|
+
// cannot import lib/), README.md and README.zh-CN.md. Exported so
|
|
348
|
+
// tests/manual-fallback-sync.test.mjs can pin the other three to this one and fail if a
|
|
349
|
+
// fifth appears; a string kept in sync by a comment is a string that drifts.
|
|
350
|
+
export const MANUAL_TARBALL_FALLBACK =
|
|
351
|
+
'T=$(mktemp -d) && U=$(curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/releases/latest | grep -o \'"tarball_url"[^,]*\' | cut -d\'"\' -f4) && curl -sL "$U" | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install';
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Whether the local marketplace clone can still be fast-forwarded.
|
|
355
|
+
*
|
|
356
|
+
* This is the near cause of the failure v6.3.0 shipped a detector for. Claude Code updates a
|
|
357
|
+
* git-source marketplace by pulling that clone; a DIRTY working tree blocks the pull, the
|
|
358
|
+
* plugin silently stops updating, and eventually the database is written by a newer
|
|
359
|
+
* claude-mem-lite than the code that has to open it. On this machine the clone was pinned 22
|
|
360
|
+
* commits behind while everything reported green.
|
|
361
|
+
*
|
|
362
|
+
* It gets dirty on its own: with a DIRECTORY-source marketplace, `${CLAUDE_PLUGIN_ROOT}`
|
|
363
|
+
* resolves inside the clone, and `scripts/launch.mjs` runs `npm install` there whenever
|
|
364
|
+
* `node_modules/better-sqlite3` is missing — which is every materialization of a new version.
|
|
365
|
+
* That install rewrites **`package-lock.json`**, which IS tracked, and that is what blocks the
|
|
366
|
+
* pull. So the plugin's own launcher can create the state that stops the plugin updating.
|
|
367
|
+
*
|
|
368
|
+
* **Do not add a `node_modules` special case here.** A first cut did, and pre-ship review
|
|
369
|
+
* measured it dead: the clone is a clone of THIS repo, whose `.gitignore` carries
|
|
370
|
+
* `/node_modules`, so `git status --porcelain` never sees it — the branch was reachable only
|
|
371
|
+
* from a fixture that omitted the `.gitignore` the real clone always has. An ignored
|
|
372
|
+
* `node_modules` also does not block a fast-forward, so reporting it would have been noise
|
|
373
|
+
* even if it were visible. The tracked-file dirt is the whole signal.
|
|
374
|
+
*
|
|
375
|
+
* Five outcomes, and `unknown` is one of them on purpose: "I could not run git" must not be
|
|
376
|
+
* reported in the same voice as "the tree is clean".
|
|
377
|
+
*
|
|
378
|
+
* Exported for tests/marketplace-clone-health.test.mjs.
|
|
379
|
+
*/
|
|
380
|
+
export function marketplaceCloneHealth(
|
|
381
|
+
dir,
|
|
382
|
+
run = (args) => execFileSync('git', args, { encoding: 'utf8', timeout: 20000 }),
|
|
383
|
+
) {
|
|
384
|
+
if (!existsSync(dir)) return { kind: 'absent' };
|
|
385
|
+
if (!existsSync(join(dir, '.git'))) return { kind: 'not-git' };
|
|
386
|
+
let porcelain;
|
|
387
|
+
try {
|
|
388
|
+
porcelain = run(['-C', dir, 'status', '--porcelain']);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
return { kind: 'unknown', reason: e?.code || e?.message || 'git failed' };
|
|
391
|
+
}
|
|
392
|
+
const entries = String(porcelain)
|
|
393
|
+
.split('\n')
|
|
394
|
+
.filter((l) => l.trim());
|
|
395
|
+
if (entries.length === 0) return { kind: 'clean' };
|
|
396
|
+
return { kind: 'dirty', count: entries.length };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The `mem-lite` / `mem` registrations in `claude mcp list` output that are NOT provided by
|
|
401
|
+
* a plugin manifest.
|
|
402
|
+
*
|
|
403
|
+
* `claude mcp list` prints one `<name>: <command>` line per server, and a plugin-provided
|
|
404
|
+
* one is named `plugin:<plugin>:<server>`. The old test — `list.includes('mem-lite:')` —
|
|
405
|
+
* matched inside `plugin:claude-mem-lite:mem-lite:`, so it could not tell the two apart and
|
|
406
|
+
* always answered "registered" for a plugin user.
|
|
407
|
+
*
|
|
408
|
+
* Deliberately named for what it MEASURES: a bare-name registration, whatever its scope.
|
|
409
|
+
* `mcp list` does not label user vs project scope on the line itself, so calling this
|
|
410
|
+
* "user-scope" would claim more than the output supports.
|
|
411
|
+
*
|
|
412
|
+
* Exported for tests/mcp-registration-parse.test.mjs.
|
|
413
|
+
*/
|
|
414
|
+
export function nonPluginMemRegistrations(listOutput) {
|
|
415
|
+
const names = [];
|
|
416
|
+
for (const line of String(listOutput ?? '').split('\n')) {
|
|
417
|
+
// Anchored, no leading whitespace: the diagnostics block below the list is indented, and
|
|
418
|
+
// its `└ [Warning] [mem-lite] mcpServers.mem-lite: …` lines are not registrations.
|
|
419
|
+
const m = /^(\S+):\s+\S/.exec(line);
|
|
420
|
+
if (!m) continue;
|
|
421
|
+
const name = m[1];
|
|
422
|
+
if (name.startsWith('plugin:')) continue;
|
|
423
|
+
if (name === 'mem-lite' || name === 'mem') names.push(name);
|
|
424
|
+
}
|
|
425
|
+
return names;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The remedy line for a `doctor` database check that threw — or null when the failure is
|
|
430
|
+
* one this cannot classify.
|
|
431
|
+
*
|
|
432
|
+
* Returning null is deliberate and is the case worth defending: a diagnostic that always
|
|
433
|
+
* prints a fix eventually prints the wrong one, and the bare error message is a better
|
|
434
|
+
* answer than a confident irrelevance. The three CLASSIFIED outcomes are kept apart for the
|
|
435
|
+
* same reason — "restore this snapshot", "there is no snapshot", and "I could not read the
|
|
436
|
+
* directory to find out" are three different situations, and collapsing the last two ends
|
|
437
|
+
* the reader's search with a fact nobody checked.
|
|
438
|
+
*
|
|
439
|
+
* Shell commands only, no `claude-mem-lite <cmd>`: the remedy for a broken store must not
|
|
440
|
+
* itself depend on which install shape the user has (the plugin cache has no CLI on PATH).
|
|
441
|
+
*
|
|
442
|
+
* Exported for tests/doctor-db-remedy.test.mjs, which also drives the shipped doctor over a
|
|
443
|
+
* corrupt file — a pure function nothing calls is the wiring gap this repo keeps finding.
|
|
444
|
+
*/
|
|
445
|
+
export function dbCheckRemedy(dbPath, err) {
|
|
446
|
+
if (isNativeBindingError(err)) return `Repair: ${nativeBindingRepairHint(PROJECT_DIR)}`;
|
|
447
|
+
const msg = String(err?.message ?? err ?? '');
|
|
448
|
+
// SQLite's own spellings for "this file is not a usable database".
|
|
449
|
+
if (!/not a database|disk image is malformed|file is not a database/i.test(msg)) return null;
|
|
450
|
+
|
|
451
|
+
const clear = `rm -f "${dbPath}-wal" "${dbPath}-shm"`;
|
|
452
|
+
const snap = readSnapshots(dbPath);
|
|
453
|
+
if (!snap.ok) {
|
|
454
|
+
return (
|
|
455
|
+
`Could not read ${dirname(dbPath)} to look for a backup snapshot (${snap.reason}) — ` +
|
|
456
|
+
`fix that directory first, then look for ${basename(dbPath)}.*.bak beside the database.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (snap.snapshots.length === 0) {
|
|
460
|
+
return (
|
|
461
|
+
`No backup snapshot exists beside the database. Set the broken file aside so a fresh ` +
|
|
462
|
+
`store is created on the next session: ${clear} && mv "${dbPath}" "${dbPath}.corrupt" ` +
|
|
463
|
+
`— memories in that file are not recoverable without a backup.`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
// Newest by mtime. Ties are broken by name, which carries an ISO stamp, so the answer is
|
|
467
|
+
// total rather than dependent on which of two same-millisecond files readdir returned
|
|
468
|
+
// first (the D#9 shape).
|
|
469
|
+
const newest = snap.snapshots
|
|
470
|
+
.slice()
|
|
471
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.path < b.path ? 1 : -1))[0];
|
|
472
|
+
return (
|
|
473
|
+
`Restore the newest of ${snap.snapshots.length} backup snapshot(s): ` +
|
|
474
|
+
`${clear} && cp "${newest.path}" "${dbPath}" ` +
|
|
475
|
+
`— move the broken file aside first if you want to keep it for inspection.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
336
479
|
// ─── Install ────────────────────────────────────────────────────────────────
|
|
337
480
|
|
|
338
481
|
// Dynamic-import helpers, resolved against the installed copy at INSTALL_DIR
|
|
@@ -1136,11 +1279,23 @@ async function uninstall() {
|
|
|
1136
1279
|
ok('Marketplace directory removed');
|
|
1137
1280
|
}
|
|
1138
1281
|
|
|
1139
|
-
// 5b. Remove cache
|
|
1282
|
+
// 5b. Remove cache directories — OURS unconditionally, the marketplace-wide one gated.
|
|
1283
|
+
//
|
|
1284
|
+
// The gate exists so uninstalling this plugin does not delete a sibling plugin published
|
|
1285
|
+
// under the same marketplace. That reasoning covers `cache/<marketplace>/`; it does not
|
|
1286
|
+
// cover `cache/<marketplace>/claude-mem-lite/`, which is ours alone. Because only the
|
|
1287
|
+
// gated branch existed, a user with any other sdsrss plugin installed kept every cached
|
|
1288
|
+
// version of THIS one — measured at 241 MB on a machine where `/plugin uninstall` had
|
|
1289
|
+
// already removed the manifest, i.e. bytes belonging to a plugin that was gone.
|
|
1290
|
+
const ownCacheDir = join(pluginsDir, 'cache', marketplaceKey, PLUGIN_NAME);
|
|
1291
|
+
if (existsSync(ownCacheDir)) {
|
|
1292
|
+
rmSync(ownCacheDir, { recursive: true, force: true });
|
|
1293
|
+
ok('Plugin cache removed');
|
|
1294
|
+
}
|
|
1140
1295
|
const cacheDir = join(pluginsDir, 'cache', marketplaceKey);
|
|
1141
1296
|
if (canRemoveMarketplaceArtifacts && existsSync(cacheDir)) {
|
|
1142
1297
|
rmSync(cacheDir, { recursive: true, force: true });
|
|
1143
|
-
ok('
|
|
1298
|
+
ok('Marketplace cache directory removed');
|
|
1144
1299
|
}
|
|
1145
1300
|
|
|
1146
1301
|
// 5c. Clean known_marketplaces.json
|
|
@@ -1224,35 +1379,52 @@ async function status() {
|
|
|
1224
1379
|
// configured` at a correctly-installed plugin user — two red marks describing
|
|
1225
1380
|
// the intended state.
|
|
1226
1381
|
const shape = detectInstallShape({ home: homedir(), projectDir: PROJECT_DIR, installDir: INSTALL_DIR });
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1382
|
+
// A cache DIRECTORY is not an installed plugin — `/plugin uninstall` leaves version dirs
|
|
1383
|
+
// behind (this project's own README documents that), and `activePluginVersion` falls back to
|
|
1384
|
+
// "newest cache dir" when nothing recorded an install. Both branches below credit the
|
|
1385
|
+
// manifest with providing something, so both need the registration, not the directory.
|
|
1386
|
+
const pluginProvides =
|
|
1387
|
+
!!shape.activePluginVersion && pluginIsRegistered({ home: homedir(), settings: readSettings() });
|
|
1388
|
+
|
|
1389
|
+
// MCP. A plugin install answers this from the manifest and does NOT shell out.
|
|
1390
|
+
//
|
|
1391
|
+
// Two reasons, and the first is correctness rather than speed. `claude mcp list` prints a
|
|
1392
|
+
// plugin server as `plugin:claude-mem-lite:mem-lite: …`, and the old substring test
|
|
1393
|
+
// `list.includes('mem-lite:')` matched INSIDE that name — so a plugin user was reported as
|
|
1394
|
+
// having a user-scope registration they do not have, and the branch written for them below
|
|
1395
|
+
// was unreachable. That is the same accidental-match class as the `\bmem\b` regex this
|
|
1396
|
+
// comment block used to describe. Second: the official help says approved servers are
|
|
1397
|
+
// "health-checked", i.e. the call STARTS every MCP server configured on the machine —
|
|
1398
|
+
// measured 2026-09-08 at 2.546s wall for three servers, one of them a remote HTTP endpoint.
|
|
1399
|
+
// A status command should not pay that, and a plugin user gains nothing from it.
|
|
1400
|
+
//
|
|
1401
|
+
// `doctor` GAINS the exec instead (it had none before this change) and runs it
|
|
1402
|
+
// unconditionally: it is the deep check, and it is where the duplicate/legacy registration
|
|
1403
|
+
// the README's "Mixed-install residue" section describes now gets detected — nothing
|
|
1404
|
+
// detected it before. That means `doctor` now health-checks every MCP server on the
|
|
1405
|
+
// machine; both READMEs say so under their `doctor` sections.
|
|
1406
|
+
if (pluginProvides) {
|
|
1407
|
+
push(
|
|
1408
|
+
'ok',
|
|
1409
|
+
'mcp',
|
|
1410
|
+
`MCP server: provided by the plugin manifest (v${shape.activePluginVersion.version} .mcp.json) — no user-scope registration expected`,
|
|
1411
|
+
{ registered: false, via: 'plugin' },
|
|
1412
|
+
);
|
|
1413
|
+
} else
|
|
1414
|
+
try {
|
|
1415
|
+
const list = execFileSync('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 60000 });
|
|
1416
|
+
// Accept either the current "mem-lite" registration or the legacy "mem" name
|
|
1417
|
+
// (pre-v2.78) so a user mid-upgrade still sees a green status until setup.sh /
|
|
1418
|
+
// install.mjs purges the legacy entry on next run.
|
|
1419
|
+
const registered = nonPluginMemRegistrations(list).length > 0;
|
|
1420
|
+
if (registered) {
|
|
1421
|
+
push('ok', 'mcp', 'MCP server: registered', { registered });
|
|
1422
|
+
} else {
|
|
1423
|
+
push('fail', 'mcp', 'MCP server: not registered', { registered });
|
|
1424
|
+
}
|
|
1425
|
+
} catch {
|
|
1426
|
+
push('warn', 'mcp', 'Could not check MCP status', { registered: null });
|
|
1252
1427
|
}
|
|
1253
|
-
} catch {
|
|
1254
|
-
push('warn', 'mcp', 'Could not check MCP status', { registered: null });
|
|
1255
|
-
}
|
|
1256
1428
|
|
|
1257
1429
|
// Hooks
|
|
1258
1430
|
const settings = readSettings();
|
|
@@ -1707,6 +1879,65 @@ async function doctor() {
|
|
|
1707
1879
|
ok('Orphan hooks: none (all hook targets present)');
|
|
1708
1880
|
}
|
|
1709
1881
|
|
|
1882
|
+
// MCP registration. This lives in doctor, not status: `claude mcp list` health-checks —
|
|
1883
|
+
// i.e. STARTS — every MCP server configured on the machine (2.546s wall for three servers,
|
|
1884
|
+
// measured 2026-09-08), which is a cost the deep check can carry and a status line cannot.
|
|
1885
|
+
//
|
|
1886
|
+
// What it buys beyond status: the DUPLICATE. The README's "Mixed-install residue" section
|
|
1887
|
+
// has warned since v3 that a plugin user who once ran the npx/git-clone installer keeps a
|
|
1888
|
+
// bare-name registration that double-registers the server — and nothing in the tool
|
|
1889
|
+
// detected it. Orphan hooks had a check; its MCP twin did not.
|
|
1890
|
+
try {
|
|
1891
|
+
const list = execFileSync('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 60000 });
|
|
1892
|
+
const bare = nonPluginMemRegistrations(list);
|
|
1893
|
+
// Registration, not directory — see pluginIsRegistered. Crediting a leftover cache dir
|
|
1894
|
+
// here told a working npm-channel install to delete its ONLY MCP registration.
|
|
1895
|
+
const viaPlugin =
|
|
1896
|
+
!!shape?.activePluginVersion && pluginIsRegistered({ home: homedir(), settings: readSettings() });
|
|
1897
|
+
if (viaPlugin && bare.length > 0) {
|
|
1898
|
+
dwarn(
|
|
1899
|
+
`MCP registration: the plugin manifest provides the server AND a bare "${bare.join('", "')}" registration exists — the server is registered twice`,
|
|
1900
|
+
);
|
|
1901
|
+
// No `-s` flag, deliberately: `nonPluginMemRegistrations`'s own docblock says `mcp list`
|
|
1902
|
+
// does not label scope, and this repo's tracked `.mcp.json` registers a bare `mem-lite`
|
|
1903
|
+
// at PROJECT scope, which `-s user` cannot remove. `claude mcp remove` without the flag
|
|
1904
|
+
// removes from whichever scope the entry is in. Every name, not just the first.
|
|
1905
|
+
for (const name of bare) log(` Fix: claude mcp remove ${name}`);
|
|
1906
|
+
} else if (viaPlugin) {
|
|
1907
|
+
ok('MCP registration: provided by the plugin manifest only (no duplicate)');
|
|
1908
|
+
} else if (bare.length > 0) {
|
|
1909
|
+
ok(`MCP registration: "${bare.join('", "')}" registered`);
|
|
1910
|
+
} else {
|
|
1911
|
+
dwarn('MCP registration: no claude-mem-lite MCP server is registered and no plugin provides one');
|
|
1912
|
+
}
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
// Third outcome, kept apart from "none found" on purpose: the `claude` CLI may not be on
|
|
1915
|
+
// PATH at all, and a green "no duplicate" would end the reader's search on a check that
|
|
1916
|
+
// never ran.
|
|
1917
|
+
dwarn(`MCP registration: could not run \`claude mcp list\` (${e.code || e.message}) — not checked`);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Marketplace clone updatability — see marketplaceCloneHealth for why this is the
|
|
1921
|
+
// precondition behind the schema-skew lock-in v6.3.0 shipped a detector for.
|
|
1922
|
+
const marketplaceClone = join(homedir(), '.claude', 'plugins', 'marketplaces', MARKETPLACE_KEY);
|
|
1923
|
+
const clone = marketplaceCloneHealth(marketplaceClone);
|
|
1924
|
+
if (clone.kind === 'dirty') {
|
|
1925
|
+
dwarn(`Marketplace clone: ${clone.count} uncommitted change(s) in ${marketplaceClone}`);
|
|
1926
|
+
log(
|
|
1927
|
+
' Claude Code updates a git-source marketplace by pulling this clone, and a dirty tree blocks the pull —',
|
|
1928
|
+
);
|
|
1929
|
+
log(
|
|
1930
|
+
' the plugin then stops updating silently, which is how a machine ends up running code older than its DB.',
|
|
1931
|
+
);
|
|
1932
|
+
log(` Inspect: git -C ${marketplaceClone} status`);
|
|
1933
|
+
} else if (clone.kind === 'unknown') {
|
|
1934
|
+
dwarn(`Marketplace clone: could not check ${marketplaceClone} (${clone.reason}) — not checked`);
|
|
1935
|
+
} else if (clone.kind === 'clean') {
|
|
1936
|
+
ok('Marketplace clone: clean (the marketplace updater can fast-forward it)');
|
|
1937
|
+
}
|
|
1938
|
+
// 'absent' / 'not-git' are silent: an npm-channel or npx user has no marketplace clone,
|
|
1939
|
+
// and a check that reports on a thing you do not have is noise.
|
|
1940
|
+
|
|
1710
1941
|
// Database
|
|
1711
1942
|
if (existsSync(DB_PATH)) {
|
|
1712
1943
|
try {
|
|
@@ -1750,6 +1981,11 @@ async function doctor() {
|
|
|
1750
1981
|
}
|
|
1751
1982
|
} catch (e) {
|
|
1752
1983
|
fail('Database: ' + e.message);
|
|
1984
|
+
// Every other ✗ on this screen carries a remedy; this one used to be the exception,
|
|
1985
|
+
// and a corrupt store is the failure a user is least able to diagnose unaided.
|
|
1986
|
+
// dbCheckRemedy returns null rather than invent one for an error it cannot classify.
|
|
1987
|
+
const remedy = dbCheckRemedy(DB_PATH, e);
|
|
1988
|
+
if (remedy) log(` ${remedy}`);
|
|
1753
1989
|
issues++;
|
|
1754
1990
|
}
|
|
1755
1991
|
} else {
|
|
@@ -2356,6 +2592,41 @@ export function hasOtherMarketplacePlugins(
|
|
|
2356
2592
|
return Object.keys(plugins).some((key) => key !== pluginKey && key.endsWith(`@${marketplaceKey}`));
|
|
2357
2593
|
}
|
|
2358
2594
|
|
|
2595
|
+
/**
|
|
2596
|
+
* Whether Claude Code actually has this plugin INSTALLED — as opposed to a leftover version
|
|
2597
|
+
* directory sitting in its cache.
|
|
2598
|
+
*
|
|
2599
|
+
* `detectInstallShape`'s `activePluginVersion` is not that question. Its own comment calls its
|
|
2600
|
+
* third tier — the newest cache directory — "a guess, and after a rollback the wrong one", and
|
|
2601
|
+
* a terminal has neither of the first two tiers (`CLAUDE_PLUGIN_ROOT`, `installed_plugins.json`)
|
|
2602
|
+
* after `/plugin uninstall`. `/plugin uninstall` leaves the version dirs behind, which this
|
|
2603
|
+
* project's own README now documents — so "a cache directory exists" is true on machines that
|
|
2604
|
+
* have no plugin at all.
|
|
2605
|
+
*
|
|
2606
|
+
* Using it as "the plugin provides the MCP server" was measured to tell a working npm-channel
|
|
2607
|
+
* install that its server was registered twice, with a remedy that removes its ONLY
|
|
2608
|
+
* registration. Read from the two places that RECORD an installation instead.
|
|
2609
|
+
*
|
|
2610
|
+
* Deliberately NOT `!shape.managed`: a mixed install has both, and that is precisely the state
|
|
2611
|
+
* the duplicate check exists for. Over-narrowing here is safe by construction — the caller
|
|
2612
|
+
* falls back to asking `claude mcp list`, which is the pre-fix behaviour and correct.
|
|
2613
|
+
*
|
|
2614
|
+
* Exported for tests/mcp-registration-parse.test.mjs.
|
|
2615
|
+
*/
|
|
2616
|
+
export function pluginIsRegistered({ home = homedir(), settings = {} } = {}) {
|
|
2617
|
+
if (isPluginExplicitlyDisabled(settings)) return false;
|
|
2618
|
+
if (settings?.enabledPlugins?.[PLUGIN_KEY] === true) return true;
|
|
2619
|
+
try {
|
|
2620
|
+
const installed = JSON.parse(
|
|
2621
|
+
readFileSync(join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8'),
|
|
2622
|
+
);
|
|
2623
|
+
return PLUGIN_KEY in getInstalledPluginEntries(installed);
|
|
2624
|
+
} catch {
|
|
2625
|
+
// Missing or unparseable registry: not evidence of an installation.
|
|
2626
|
+
return false;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2359
2630
|
/** Thrown when settings.json exists but is not parseable. Never a reason to write. */
|
|
2360
2631
|
class SettingsUnparseableError extends Error {}
|
|
2361
2632
|
|
|
@@ -2624,9 +2895,7 @@ async function repair() {
|
|
|
2624
2895
|
console.log(' Automatic repair fails closed rather than run unverified code.');
|
|
2625
2896
|
console.log(' Manual fallback — run this in any shell (you are choosing to trust it):');
|
|
2626
2897
|
console.log('');
|
|
2627
|
-
console.log(
|
|
2628
|
-
' 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',
|
|
2629
|
-
);
|
|
2898
|
+
console.log(` ${MANUAL_TARBALL_FALLBACK}`);
|
|
2630
2899
|
console.log('');
|
|
2631
2900
|
process.exit(1);
|
|
2632
2901
|
} finally {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three location constants, in a leaf module.
|
|
3
|
+
*
|
|
4
|
+
* They lived in `schema.mjs`, which statically imports `better-sqlite3`. That made the
|
|
5
|
+
* native driver a LOAD-TIME dependency of anything that wanted a path — including
|
|
6
|
+
* `hook-update.mjs`, which `install.mjs::repair()` imports to reach the Ed25519-verified
|
|
7
|
+
* release path. Measured 2026-09-08: on a tree with no `node_modules`, that import threw
|
|
8
|
+
* `ERR_MODULE_NOT_FOUND` from `schema.mjs`, repair() caught it, refused to auto-install
|
|
9
|
+
* unverified code, and printed the unverified default-branch tarball instead. So the
|
|
10
|
+
* signature check was unreachable on the one install state the self-heal exists to fix.
|
|
11
|
+
*
|
|
12
|
+
* Keep this module free of package imports — `node:` builtins and `lib/resolve-data-dir.mjs`
|
|
13
|
+
* only. `tests/repair-path-no-native-dep.test.mjs` walks the static import graph from
|
|
14
|
+
* `hook-update.mjs` and fails on ANY package edge, so a future import here has to argue
|
|
15
|
+
* with a test rather than silently disarm the repair path.
|
|
16
|
+
*
|
|
17
|
+
* `schema.mjs` re-exports all three names, so every existing importer keeps working and
|
|
18
|
+
* this is not a contract change.
|
|
19
|
+
*/
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { resolveDataDir } from './resolve-data-dir.mjs';
|
|
23
|
+
|
|
24
|
+
// DATA location — DB, managed resources, registry DB, runtime/. Honors
|
|
25
|
+
// CLAUDE_MEM_DIR so users can relocate state to a larger/faster volume.
|
|
26
|
+
export const DB_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
27
|
+
export const DB_PATH = join(DB_DIR, 'claude-mem-lite.db');
|
|
28
|
+
// CODE / install location — server.mjs, hook.mjs, cli.mjs, package.json live
|
|
29
|
+
// here. ALWAYS homedir-rooted: Claude Code's settings.json + MCP registration
|
|
30
|
+
// bake ABSOLUTE paths to server.mjs/hooks, so the code must NOT follow the
|
|
31
|
+
// CLAUDE_MEM_DIR relocation env var (mirrors install.mjs INSTALL_DIR). Equals
|
|
32
|
+
// DB_DIR when CLAUDE_MEM_DIR is unset — the common, non-relocated case.
|
|
33
|
+
export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
package/lib/db-backup.mjs
CHANGED
|
@@ -43,26 +43,51 @@ export const BACKUP_EVICTION_GRACE_MS = 7 * DAY_MS;
|
|
|
43
43
|
// user's hand-made `cp db db.before-upgrade.bak` must never be auto-unlinked.
|
|
44
44
|
const SNAPSHOT_STAMP_RE = /-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-\d+-\d+\.bak$/;
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Snapshots for `dbPath`, keeping "there are none" and "I could not look" APART.
|
|
48
|
+
*
|
|
49
|
+
* `listSnapshots` below collapses both into `[]`, which is fine for its callers (a budget
|
|
50
|
+
* sweep has nothing to do either way) and wrong for anything that REPORTS to a human: a
|
|
51
|
+
* diagnostic that says "no backup exists" when it actually could not read the directory
|
|
52
|
+
* ends the reader's search with a false fact. Same rule the doctor bash-hook check
|
|
53
|
+
* settled on — "nothing to check" and "I could not look" are different answers.
|
|
54
|
+
*
|
|
55
|
+
* @returns {{ok: true, snapshots: Array<{path:string,size:number,mtimeMs:number}>}
|
|
56
|
+
* | {ok: false, reason: string}}
|
|
57
|
+
*/
|
|
58
|
+
export function readSnapshots(dbPath) {
|
|
59
|
+
let names;
|
|
60
|
+
let dir;
|
|
61
|
+
let prefix;
|
|
62
|
+
// dirname/basename inside the try, not outside it: `listSnapshots` below has always been
|
|
63
|
+
// total (it returned [] for any input), and one of this function's callers runs inside
|
|
64
|
+
// `doctor`'s DB catch block, where the repo's openDb precedent says nothing may throw.
|
|
65
|
+
// Computing the path outside would make listSnapshots(undefined) a TypeError.
|
|
48
66
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
dir = dirname(dbPath);
|
|
68
|
+
prefix = `${basename(dbPath)}.`;
|
|
69
|
+
names = readdirSync(dir);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { ok: false, reason: e?.code || e?.message || 'unreadable' };
|
|
72
|
+
}
|
|
73
|
+
const snapshots = [];
|
|
74
|
+
for (const n of names) {
|
|
75
|
+
if (!n.startsWith(prefix) || !n.endsWith('.bak')) continue;
|
|
76
|
+
const full = join(dir, n);
|
|
77
|
+
try {
|
|
78
|
+
const st = statSync(full);
|
|
79
|
+
snapshots.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
80
|
+
} catch {
|
|
81
|
+
/* raced away */
|
|
61
82
|
}
|
|
62
|
-
return out;
|
|
63
|
-
} catch {
|
|
64
|
-
return [];
|
|
65
83
|
}
|
|
84
|
+
return { ok: true, snapshots };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** All `<db>.<tag>-<ts>.bak` snapshots for `dbPath`, any tag, with size + mtime. */
|
|
88
|
+
export function listSnapshots(dbPath) {
|
|
89
|
+
const r = readSnapshots(dbPath);
|
|
90
|
+
return r.ok ? r.snapshots : [];
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
/**
|
package/lib/plugin-key.mjs
CHANGED
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
/** Marketplace this plugin is published under. */
|
|
25
25
|
export const MARKETPLACE_KEY = 'sdsrss';
|
|
26
26
|
|
|
27
|
+
/** This plugin's own name — the directory Claude Code materializes versions under. */
|
|
28
|
+
export const PLUGIN_NAME = 'claude-mem-lite';
|
|
29
|
+
|
|
27
30
|
/** The key Claude Code writes under `enabledPlugins` in `~/.claude/settings.json`. */
|
|
28
|
-
export const PLUGIN_KEY =
|
|
31
|
+
export const PLUGIN_KEY = `${PLUGIN_NAME}@${MARKETPLACE_KEY}`;
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Whether the user has EXPLICITLY switched the plugin off.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.
|
|
9
|
+
"version": "6.4.0",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"format": "prettier --write \"**/*.{mjs,js}\"",
|
|
25
25
|
"format:check": "prettier --check \"**/*.{mjs,js}\"",
|
|
26
26
|
"dead-code": "knip",
|
|
27
|
+
"validate:manifests": "node scripts/validate-plugin-manifests.mjs",
|
|
27
28
|
"audit:metrics": "node scripts/audit-metrics.mjs --md",
|
|
28
29
|
"audit:baseline": "node scripts/audit-metrics.mjs --run-tests --md",
|
|
29
30
|
"test": "vitest run",
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"lib/err-sampler.mjs",
|
|
90
91
|
"lib/hook-telemetry.mjs",
|
|
91
92
|
"lib/resolve-data-dir.mjs",
|
|
93
|
+
"lib/data-paths.mjs",
|
|
92
94
|
"lib/export-columns.mjs",
|
|
93
95
|
"lib/file-intel.mjs",
|
|
94
96
|
"lib/reread-guard.mjs",
|
package/schema.mjs
CHANGED
|
@@ -7,22 +7,20 @@ import { homedir } from 'os';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, chmodSync } from 'fs';
|
|
9
9
|
import { OBS_FTS_COLUMNS, debugCatch } from './utils.mjs';
|
|
10
|
-
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
11
10
|
// Imported, never re-declared: a hand-copied marker string is this repo's twin-drift
|
|
12
11
|
// class, and every consumer of the forward-incompat throw keys on this exact value.
|
|
13
12
|
// schema-skew.mjs imports nothing local, so this closes no cycle.
|
|
14
13
|
import { SCHEMA_SKEW_CODE } from './lib/schema-skew.mjs';
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
|
15
|
+
// The three location constants now live in lib/data-paths.mjs — a leaf module with no
|
|
16
|
+
// package imports — and are re-exported here so every existing importer is unchanged.
|
|
17
|
+
// This file statically imports better-sqlite3, so holding a path constant here made the
|
|
18
|
+
// native driver a load-time dependency of anything that wanted one; that is what put the
|
|
19
|
+
// Ed25519-verified repair path out of reach on a tree with no node_modules. Imported AND
|
|
20
|
+
// re-exported (not `export … from`) because schema.mjs uses DB_DIR / DB_PATH itself.
|
|
21
|
+
// See lib/data-paths.mjs and tests/repair-path-no-native-dep.test.mjs.
|
|
22
|
+
import { DB_DIR, DB_PATH, CODE_DIR } from './lib/data-paths.mjs';
|
|
23
|
+
export { DB_DIR, DB_PATH, CODE_DIR };
|
|
26
24
|
|
|
27
25
|
// Increment when schema changes (tables, columns, indexes, FTS, migrations)
|
|
28
26
|
//
|
|
@@ -31,6 +31,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
31
31
|
import { dirname, join, isAbsolute } from 'node:path';
|
|
32
32
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
33
|
import { homedir } from 'node:os';
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
const INSTALL_DIR = join(__dirname, '..');
|
|
@@ -65,7 +66,16 @@ const DATA_DIR = MEM_DIR && isAbsolute(MEM_DIR) ? MEM_DIR : join(homedir(), '.cl
|
|
|
65
66
|
// `tests/runtime-dir-single-home.test.mjs` asserts this file still carries the rule.
|
|
66
67
|
const RUNTIME_DIR = join(DATA_DIR, 'runtime'); // runtime-dir:stays-put — serves swap-in-progress only; HOOK_RUNTIME_DIR carries the hook markers
|
|
67
68
|
const HOOK_RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || RUNTIME_DIR;
|
|
68
|
-
|
|
69
|
+
// Per CODE HOME, not per machine. The runtime dir is data-dir-relative and therefore SHARED
|
|
70
|
+
// by every install shape on this box — a plugin cache version, a managed ~/.claude-mem-lite,
|
|
71
|
+
// a dev checkout. With one global name, a failed heal attempt for root A silenced root B's
|
|
72
|
+
// heal for the next six hours, and the two are repaired by different commands. The suffix is
|
|
73
|
+
// derived from INSTALL_DIR, which is what `cli.mjs repair` below actually acts on.
|
|
74
|
+
//
|
|
75
|
+
// node:crypto only — the pure-`node:` charter above still holds. No migration: a pre-6.4.0
|
|
76
|
+
// unsuffixed marker is simply ignored, which costs at most one extra heal attempt once.
|
|
77
|
+
const INSTALL_KEY = createHash('sha256').update(INSTALL_DIR).digest('hex').slice(0, 12);
|
|
78
|
+
const HEAL_MARKER = join(HOOK_RUNTIME_DIR, `hook-launcher-lastheal-${INSTALL_KEY}`);
|
|
69
79
|
const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
70
80
|
// Observable breakage state: written when the launcher degrades a broken install
|
|
71
81
|
// to exit 0, cleared once the install is confirmed healthy. `doctor` reads it so
|
|
@@ -89,6 +99,14 @@ const BROKEN_MARKER = join(HOOK_RUNTIME_DIR, 'hook-launcher-broken');
|
|
|
89
99
|
// SESSION-START only — never on the per-tool hot path, where an npm run would
|
|
90
100
|
// stall the user's edit.
|
|
91
101
|
// Marker dir: HOOK_RUNTIME_DIR (see its definition above for why it is override-aware).
|
|
102
|
+
// BOTH stay machine-wide, unlike HEAL_MARKER above, and the difference is the SUBJECT of the
|
|
103
|
+
// repair. HEAL_MARKER gates `cli.mjs repair`, which fixes THIS install dir. This pair gates
|
|
104
|
+
// the native-binding rebuild, and `install.mjs rebuildBinding()` iterates
|
|
105
|
+
// `shape.runtimeRoots` — "Every code home on this machine, not just the one this file sits
|
|
106
|
+
// in", as its own comment puts it, which is also what install.mjs tells the user. Keying it
|
|
107
|
+
// per code home would let N homes each spawn npm within one 6h window for a repair that
|
|
108
|
+
// already covered all of them. A first cut of this change did exactly that; pre-ship review
|
|
109
|
+
// caught that the justifying comment was false for the command it gates.
|
|
92
110
|
const NB_BROKEN_MARKER = join(HOOK_RUNTIME_DIR, 'native-binding-broken');
|
|
93
111
|
const NB_HEAL_MARKER = join(HOOK_RUNTIME_DIR, 'native-binding-lastheal');
|
|
94
112
|
// Literal, not imported: the pure-`node:` charter above forbids importing lib/
|
|
@@ -111,10 +129,13 @@ const CLI_REPAIR = `node ${join(INSTALL_DIR, 'cli.mjs')} repair`;
|
|
|
111
129
|
|
|
112
130
|
// Last-resort recovery string for users whose `cli.mjs repair` path
|
|
113
131
|
// itself failed (install.mjs missing / repair errored / retry still drifting).
|
|
114
|
-
// Duplicated
|
|
115
|
-
//
|
|
132
|
+
// Duplicated from install.mjs::MANUAL_TARBALL_FALLBACK — the pure-`node:` charter above
|
|
133
|
+
// forbids importing it, and this path is reachable exactly when local scripts are broken.
|
|
134
|
+
// Pinned to that constant by tests/manual-fallback-sync.test.mjs, which also fails if a
|
|
135
|
+
// fifth surface starts hardcoding its own. Resolves the latest RELEASE tag rather than
|
|
136
|
+
// `/tarball` (the default branch, i.e. unreleased WIP).
|
|
116
137
|
const TARBALL_FALLBACK =
|
|
117
|
-
'T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/
|
|
138
|
+
'T=$(mktemp -d) && U=$(curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/releases/latest | grep -o \'"tarball_url"[^,]*\' | cut -d\'"\' -f4) && curl -sL "$U" | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install';
|
|
118
139
|
|
|
119
140
|
const [, , entryArg, ...rest] = process.argv;
|
|
120
141
|
if (!entryArg) {
|
package/server.mjs
CHANGED
|
@@ -7,7 +7,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { truncate, typeIcon, inferProject, fmtDate, debugLog, debugCatch } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
|
-
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR } from './schema.mjs';
|
|
10
|
+
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
|
|
11
|
+
// schema.mjs already imports this module for SCHEMA_SKEW_CODE, so it is in the graph before
|
|
12
|
+
// the DB is touched — a static import here adds no cold-start cost.
|
|
13
|
+
import {
|
|
14
|
+
isSchemaSkewError,
|
|
15
|
+
schemaSkewFromError,
|
|
16
|
+
schemaSkewRemedy,
|
|
17
|
+
formatSchemaSkewNotice,
|
|
18
|
+
} from './lib/schema-skew.mjs';
|
|
11
19
|
import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
20
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
21
|
import {
|
|
@@ -153,6 +161,54 @@ try {
|
|
|
153
161
|
info: (m) => debugLog('INFO', 'server', m),
|
|
154
162
|
});
|
|
155
163
|
} catch (err) {
|
|
164
|
+
// Schema skew gets the same treatment as the CLI (mem-cli.mjs) and for the same reason:
|
|
165
|
+
// the raw message ends in `npm i -g claude-mem-lite@latest`, which repairs nothing on the
|
|
166
|
+
// plugin-cache install that actually hits this.
|
|
167
|
+
//
|
|
168
|
+
// This branch has to live HERE rather than in scripts/launch.mjs, which already knows how
|
|
169
|
+
// to format the notice. server.mjs opens the DB while it is being imported, so the throw
|
|
170
|
+
// is caught by this block and `process.exit(1)` runs before the launcher's own catch can
|
|
171
|
+
// ever see it — the launcher's handler is unreachable for this one error. Re-throwing
|
|
172
|
+
// instead would fix the plugin path and break the npm one, because install.mjs registers
|
|
173
|
+
// `claude mcp add ... -- node <SERVER_PATH>`, launching this file with no launcher above
|
|
174
|
+
// it to catch anything. Emitting from here covers both, and the launcher's copy stays as
|
|
175
|
+
// the handler for failures that happen before this module is reached.
|
|
176
|
+
if (isSchemaSkewError(err)) {
|
|
177
|
+
const skew = schemaSkewFromError(err) || { dbVersion: null, binaryVersion: null };
|
|
178
|
+
let shape = { managed: false, activePluginVersion: null };
|
|
179
|
+
let dev = false;
|
|
180
|
+
try {
|
|
181
|
+
const [shapeMod, updateMod] = await Promise.all([
|
|
182
|
+
import('./lib/install-shape.mjs'),
|
|
183
|
+
import('./hook-update.mjs'),
|
|
184
|
+
]);
|
|
185
|
+
// CODE_DIR, not DB_DIR: `hasManagedCodeInstall` looks for server.mjs + hook.mjs inside
|
|
186
|
+
// whatever it is handed, and DB_DIR follows CLAUDE_MEM_DIR. Handing it the relocated DATA
|
|
187
|
+
// dir reports `managed: false` for a machine that has a managed install, so the remedy
|
|
188
|
+
// came out as "could not identify this install" or, with any plugin cache present, the
|
|
189
|
+
// plugin commands printed under a line naming ~/.claude-mem-lite. The three sibling call
|
|
190
|
+
// sites (mem-cli.mjs, hook.mjs, scripts/launch.mjs) all pass the CODE dir.
|
|
191
|
+
shape = shapeMod.detectInstallShape({ installDir: CODE_DIR });
|
|
192
|
+
dev = updateMod.isDevMode();
|
|
193
|
+
} catch {
|
|
194
|
+
/* shape unknown → schemaSkewRemedy answers 'unknown', which is its job */
|
|
195
|
+
}
|
|
196
|
+
const codeHome = process.env.CLAUDE_PLUGIN_ROOT || import.meta.dirname;
|
|
197
|
+
console.error(
|
|
198
|
+
formatSchemaSkewNotice({
|
|
199
|
+
dbVersion: skew.dbVersion,
|
|
200
|
+
binaryVersion: skew.binaryVersion,
|
|
201
|
+
remedy: schemaSkewRemedy({
|
|
202
|
+
managed: shape.managed,
|
|
203
|
+
activePluginVersion: shape.activePluginVersion,
|
|
204
|
+
dev,
|
|
205
|
+
root: codeHome,
|
|
206
|
+
}),
|
|
207
|
+
codeHome,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
156
212
|
// Fatal: log and exit with descriptive message (Claude Code shows stderr)
|
|
157
213
|
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
|
|
158
214
|
if (err.walRecoveryAttempted) {
|
package/source-files.mjs
CHANGED
|
@@ -55,6 +55,11 @@ export const SOURCE_FILES = [
|
|
|
55
55
|
// AND hook scripts (pre-tool-recall / post-tool-recall) — ship it
|
|
56
56
|
// or auto-update leaves schema + every hook with ERR_MODULE_NOT_FOUND on each fire.
|
|
57
57
|
'lib/resolve-data-dir.mjs',
|
|
58
|
+
// DB_DIR / DB_PATH / CODE_DIR. Statically imported by schema.mjs (which re-exports all
|
|
59
|
+
// three) and by hook-update.mjs, which takes them from HERE so the verified repair path
|
|
60
|
+
// stays loadable without better-sqlite3. Missing from the manifest → auto-update leaves
|
|
61
|
+
// schema.mjs and the repair path with ERR_MODULE_NOT_FOUND on every fire.
|
|
62
|
+
'lib/data-paths.mjs',
|
|
58
63
|
// lib/ — statically imported by hook-llm.mjs (activity) + hook-handoff.mjs (git-state, task-reader);
|
|
59
64
|
// dynamically imported by hook.mjs (startup-dashboard) + mem-cli.mjs (doctor-benchmark, plan-reader).
|
|
60
65
|
'lib/activity.mjs',
|
package/tool-schemas.mjs
CHANGED
|
@@ -256,7 +256,15 @@ export const memGetSchema = {
|
|
|
256
256
|
};
|
|
257
257
|
|
|
258
258
|
export const memDeleteSchema = {
|
|
259
|
-
|
|
259
|
+
// `.nonoptional()` is not a runtime change — zod already rejected an omitted `ids` here.
|
|
260
|
+
// It is what keeps the field in the PUBLISHED JSON Schema's `required` array: zod 4's
|
|
261
|
+
// toJSONSchema({io:'input'}) reads a ZodPipe's input side as accepting `undefined` and
|
|
262
|
+
// drops the key, so the advertised contract said optional while the server said required.
|
|
263
|
+
// See tests/tool-schemas.test.mjs, which grades every field against runtime ground truth.
|
|
264
|
+
ids: coerceIntArray
|
|
265
|
+
.pipe(z.array(z.number().int()).min(1).max(50))
|
|
266
|
+
.nonoptional()
|
|
267
|
+
.describe('Observation IDs to delete'),
|
|
260
268
|
confirm: coerceBool.describe('false=preview what will be deleted, true=execute deletion'),
|
|
261
269
|
};
|
|
262
270
|
|
|
@@ -433,7 +441,8 @@ export const memMaintainSchema = {
|
|
|
433
441
|
};
|
|
434
442
|
|
|
435
443
|
export const memUpdateSchema = {
|
|
436
|
-
|
|
444
|
+
// `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
|
|
445
|
+
id: coerceInt.pipe(z.number().int().positive()).nonoptional().describe('Observation ID to update'),
|
|
437
446
|
// CLI parity (cmdUpdate): empty/whitespace title would render as `(untitled)`
|
|
438
447
|
// in every listing — reject here like the CLI does, instead of persisting it.
|
|
439
448
|
title: z
|
|
@@ -541,6 +550,10 @@ export const memDeferDropSchema = {
|
|
|
541
550
|
coerceInt.pipe(z.number().int().positive()),
|
|
542
551
|
z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
|
|
543
552
|
])
|
|
553
|
+
// `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
|
|
554
|
+
// This is the CORE tool of the three — it ships in tools/list, so the drift was visible
|
|
555
|
+
// to every agent, and the tool's own description already calls the reason "required".
|
|
556
|
+
.nonoptional()
|
|
544
557
|
.describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
|
|
545
558
|
reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
|
|
546
559
|
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|