claude-mem-lite 6.2.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.
@@ -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.2.0",
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.2.0",
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, and stale processes.
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/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
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/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install
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-shared.mjs CHANGED
@@ -22,6 +22,12 @@ import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
22
22
  // Pure-`node:`/local module (it imports only binding-probe + native-binding-hint, and
23
23
  // neither imports this file) — no cycle.
24
24
  import { recordHookError } from './lib/hook-telemetry.mjs';
25
+ import {
26
+ isSchemaSkewError,
27
+ schemaSkewFromError,
28
+ shouldRecordSkew,
29
+ SKEW_MARKER_PREFIX,
30
+ } from './lib/schema-skew.mjs';
25
31
  // Audit 2026-09-05 P1-2 (carried from 2026-09-02 P2-9): `callLLM`, the quiet/adoption
26
32
  // predicates and the handoff constants moved into `lib/` because two lib modules
27
33
  // imported them from here and dragged this file's whole import graph — haiku-client,
@@ -237,6 +243,7 @@ export const GC_PROJECT_MARKER_PREFIXES = Object.freeze([
237
243
  // forever. `.skill-cooldown-` / `.skill-reco-cooldown-` left with the skill registry in
238
244
  // v5.0.0; a prefix for files nothing writes any more is dead weight in a hot-path loop.
239
245
  'last-mark-compressible-', // per-project auto-compress 24h gate
246
+ SKEW_MARKER_PREFIX, // per-project schema-skew log dedup; regenerated on the next skewed open
240
247
  ]);
241
248
 
242
249
  // Records of a completed side effect — never age out. `ep-`/`ep-flush-`/
@@ -382,12 +389,58 @@ export function createSessionId() {
382
389
 
383
390
  // ─── Database ────────────────────────────────────────────────────────────────
384
391
 
392
+ // Last forward-incompat ("the DB is newer than me") failure seen in THIS process, or null.
393
+ // SessionStart needs the two version numbers to render its notice and openDb has just been
394
+ // handed them, so this beats a second DB open — and on a skew there may be no working
395
+ // binding to open with anyway.
396
+ let lastSkew = null;
397
+
398
+ /**
399
+ * The schema skew that made the most recent openDb() return null, or null.
400
+ * Cleared by any successful open, so a heal mid-session stops the notice.
401
+ *
402
+ * @returns {{dbVersion: number|null, binaryVersion: number|null}|null}
403
+ */
404
+ export function lastSchemaSkew() {
405
+ return lastSkew;
406
+ }
407
+
385
408
  export function openDb() {
386
409
  try {
387
410
  // WAL-corruption self-heal (was server.mjs-only): without it, hooks stayed
388
411
  // silently dead (null DB) on a corrupt WAL until the next MCP server start.
389
- return ensureDbWithWalRecovery();
412
+ const db = ensureDbWithWalRecovery();
413
+ lastSkew = null;
414
+ return db;
390
415
  } catch (e) {
416
+ // Forward-incompat is its own family: it cannot be healed by anything this process can
417
+ // do, it repeats on every single open, and it is the one failure the USER has to act on.
418
+ // Record it once and hand the numbers to SessionStart, which is the surface that speaks.
419
+ // Forward-incompat is its own family: nothing this process can do heals it, it repeats on
420
+ // every single open, and it is the one failure the USER has to act on. Dedup lives in
421
+ // lib/schema-skew.mjs so the `ups` face — which opens the DB itself and logged its own 15
422
+ // of the day's 727 lines — shares one implementation instead of drifting from this one.
423
+ //
424
+ // shouldRecordSkew is TOTAL by contract. Nothing in this catch may throw: the first cut
425
+ // called getSessionId() here, which MINTS and writes a session id, so an unwritable
426
+ // runtime dir turned openDb() itself into a thrower. All 13 call sites are written to
427
+ // no-op on null and none of them expects an exception.
428
+ if (isSchemaSkewError(e)) {
429
+ lastSkew = schemaSkewFromError(e) || { dbVersion: null, binaryVersion: null };
430
+ // Guarded even though inferProject() reads env and cwd: "the only statement in this
431
+ // catch cannot throw" was true of the original one-line body and stopped being true
432
+ // the moment anything was added. An unscoped marker is a worse dedup, not a crash.
433
+ let project = '';
434
+ try {
435
+ project = inferProject();
436
+ } catch {
437
+ /* total: the marker degrades to one shared file */
438
+ }
439
+ if (shouldRecordSkew(RUNTIME_DIR, project, lastSkew)) {
440
+ recordHookError('hook-shared:db-open', e, RUNTIME_DIR);
441
+ }
442
+ return null;
443
+ }
391
444
  // Still null, still no throw — a hook must never crash the host session, and all
392
445
  // eight call sites in hook.mjs are written to no-op on null. But "returned null"
393
446
  // used to be the ONLY trace: nothing reached runtime/hook-errors/, so `stats`
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
- import { DB_DIR, CODE_DIR } from './schema.mjs';
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
@@ -228,7 +233,11 @@ function isPluginMode() {
228
233
  }
229
234
 
230
235
  // ── Dev Mode Detection ─────────────────────────────────────
231
- function isDevMode() {
236
+ // Exported since the schema-skew notice needs it: a dev checkout must be told `git pull`,
237
+ // never a command that would overwrite its working tree. Re-implementing the check at the
238
+ // call site would make it the second copy of a predicate this file has already had to get
239
+ // right twice (whole-dir symlink, then per-file drift) — the twin-drift class.
240
+ export function isDevMode() {
232
241
  try {
233
242
  // A dev checkout always carries a .git dir. This catches a whole-directory
234
243
  // symlink (~/.claude-mem-lite -> /repo): lstat on server.mjs there follows the
package/hook.mjs CHANGED
@@ -60,7 +60,9 @@ import {
60
60
  episodeHasSignificantContent,
61
61
  explainSignificance,
62
62
  } from './hook-episode.mjs';
63
- import { DB_DIR } from './schema.mjs';
63
+ // CODE_DIR, not DB_DIR: the schema-skew notice asks which CODE homes exist, and those are
64
+ // always homedir-rooted even when CLAUDE_MEM_DIR relocates the data.
65
+ import { DB_DIR, CODE_DIR } from './schema.mjs';
64
66
  import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
65
67
  import { entry as preCompactEntry } from './hook-precompact.mjs';
66
68
  import {
@@ -82,6 +84,7 @@ import {
82
84
  spawnBackground,
83
85
  sweepOrphanEpisodeFiles,
84
86
  sweepStaleProjectMarkers,
87
+ lastSchemaSkew,
85
88
  } from './hook-shared.mjs';
86
89
  import { handleLLMEpisode, handleLLMSummary, saveEpisodeImmediate } from './hook-llm.mjs';
87
90
  import { readFastSummarySource, insertFastSummary, FAST_SUMMARY_LIMITS } from './lib/fast-summary.mjs';
@@ -2290,6 +2293,67 @@ async function buildStartupDashboardText(db, project) {
2290
2293
  }
2291
2294
  }
2292
2295
 
2296
+ /**
2297
+ * Tell the user their memory is version-skewed, on the one surface they read.
2298
+ *
2299
+ * Only fires when openDb() failed for THIS reason — hook-shared records the two version
2300
+ * numbers as it catches, so nothing is re-derived and no second DB open is attempted (on a
2301
+ * skew there may be no usable binding to open with).
2302
+ *
2303
+ * Everything is dynamically imported: this is a cold path that must not cost the healthy
2304
+ * SessionStart an install-shape scan. And it goes through the queue helpers, never a bare
2305
+ * console.log — SessionStart merges three would-be stdout contributors into ONE envelope,
2306
+ * and writing raw prose alongside it once made the host deliver the whole JSON object to
2307
+ * the model as literal text (tests/session-start-stdout-envelope.test.mjs).
2308
+ */
2309
+ async function emitSchemaSkewNotice() {
2310
+ try {
2311
+ const skew = lastSchemaSkew();
2312
+ if (!skew) return;
2313
+ const [shapeMod, updateMod, skewMod] = await Promise.all([
2314
+ import('./lib/install-shape.mjs'),
2315
+ import('./hook-update.mjs'),
2316
+ import('./lib/schema-skew.mjs'),
2317
+ ]);
2318
+ const shape = shapeMod.detectInstallShape({ installDir: CODE_DIR });
2319
+ // WHICH tree is running this hook, not which trees exist. CLAUDE_PLUGIN_ROOT is set in
2320
+ // every hook process Claude Code spawns, so on a machine holding BOTH a managed install
2321
+ // and a plugin cache it is the only thing that knows which one is behind. Deciding from
2322
+ // the machine's global shape printed `claude-mem-lite self-update` beneath a line naming
2323
+ // the plugin cache — a repair that cannot advance the tree it had just named.
2324
+ const runningRoot = process.env.CLAUDE_PLUGIN_ROOT || CODE_DIR;
2325
+ const remedy = skewMod.schemaSkewRemedy({
2326
+ managed: shape.managed,
2327
+ activePluginVersion: shape.activePluginVersion,
2328
+ dev: updateMod.isDevMode(),
2329
+ root: runningRoot,
2330
+ });
2331
+ const notice = skewMod.formatSchemaSkewNotice({
2332
+ dbVersion: skew.dbVersion,
2333
+ binaryVersion: skew.binaryVersion,
2334
+ remedy,
2335
+ // Name the home only when the remedy is about that home, so the two can never disagree.
2336
+ codeHome:
2337
+ remedy.kind === 'plugin' && shape.activePluginVersion
2338
+ ? `plugin cache v${shape.activePluginVersion.version}`
2339
+ : undefined,
2340
+ });
2341
+ // BOTH channels, and the HUMAN one is the point. queueHookContext reaches the model;
2342
+ // lib/hook-stdout.mjs's queueHookSystemMessage is documented "for the HUMAN, not the
2343
+ // model" and names v3.70.0 for making exactly this mistake — folding a banner into
2344
+ // additionalContext "kept its content and lost its audience". A notice whose whole job is
2345
+ // to hand the user a command must not depend on the assistant volunteering it.
2346
+ // flushHookStdout merges both into one envelope, so this is additive: the model learns
2347
+ // memory is unavailable, the user gets the repair.
2348
+ queueHookSystemMessage(notice);
2349
+ queueHookContext('SessionStart', notice);
2350
+ } catch (e) {
2351
+ // A hook must never crash the host session, and a notice that cannot render is still
2352
+ // better handled by staying quiet than by taking SessionStart down with it.
2353
+ debugCatch(e, 'session-start-schema-skew');
2354
+ }
2355
+ }
2356
+
2293
2357
  async function handleSessionStart() {
2294
2358
  // GC stale per-session cooldown files. Cheap (<5ms typical) and idempotent;
2295
2359
  // moved here from pre-tool-recall.js's hot path.
@@ -2471,7 +2535,16 @@ async function handleSessionStart() {
2471
2535
  const project = inferProject();
2472
2536
 
2473
2537
  const db = openDb();
2474
- if (!db) return;
2538
+ if (!db) {
2539
+ // A null DB used to end SessionStart in total silence. For most causes that is right —
2540
+ // they are transient, or a repair path is already running. Forward-incompat is neither:
2541
+ // it persists until the user installs newer code, it disables every write path, and the
2542
+ // only other signal it produces is a `-32000 Connection closed` from the MCP host, which
2543
+ // names nothing. Measured 2026-09-08: a whole day of it, >=648 log lines, zero words to
2544
+ // the user. This is the surface the user actually reads.
2545
+ await emitSchemaSkewNotice();
2546
+ return;
2547
+ }
2475
2548
 
2476
2549
  try {
2477
2550
  const now = new Date();