claude-mem-lite 2.93.0 → 2.95.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.93.0",
13
+ "version": "2.95.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": "2.93.0",
3
+ "version": "2.95.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/hook-update.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { execSync, execFileSync } from 'node:child_process';
6
6
  import { readFileSync, writeFileSync, copyFileSync, cpSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync, chmodSync } from 'node:fs';
7
- import { join, dirname } from 'node:path';
7
+ import { join, dirname, resolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
9
  import { tmpdir, homedir } from 'node:os';
10
10
  import { DB_DIR, CODE_DIR } from './schema.mjs';
@@ -356,7 +356,15 @@ export function validateExtractedTarball(sourceDir, expectedVersion, expectedNam
356
356
  return { ok: true };
357
357
  }
358
358
 
359
- export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
359
+ // opts.skipNpmInstall copy + atomically switch the source files WITHOUT
360
+ // running `npm install` in staging. Used by syncDataDirFromCache: when the
361
+ // source is a local plugin-cache version (not a downloaded tarball), the
362
+ // target data dir already carries a working, ABI-correct node_modules, so a
363
+ // reinstall is pure cost. With staging holding no node_modules the switch loop
364
+ // below skips the 'node_modules' switchable path (existsSync guard), leaving
365
+ // the target's node_modules untouched. Dependency bumps still flow through the
366
+ // GitHub-tarball path (downloadAndInstall), which keeps skipNpmInstall=false.
367
+ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR, opts = {}) {
360
368
  const ts = `${Date.now()}-${process.pid}`;
361
369
  const stagingDir = join(targetDir, `.update-staging-${ts}`);
362
370
  const backupDir = join(targetDir, `.update-backup-${ts}`);
@@ -371,11 +379,13 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
371
379
  mkdirSync(backupDir, { recursive: true });
372
380
 
373
381
  copyReleaseIntoStaging(sourceDir, stagingDir, manifest);
374
- execSync(NPM_INSTALL_CMD, {
375
- cwd: stagingDir,
376
- timeout: 60000,
377
- stdio: 'pipe',
378
- });
382
+ if (!opts.skipNpmInstall) {
383
+ execSync(NPM_INSTALL_CMD, {
384
+ cwd: stagingDir,
385
+ timeout: 60000,
386
+ stdio: 'pipe',
387
+ });
388
+ }
379
389
 
380
390
  for (const relPath of switchablePaths) {
381
391
  const stagedPath = join(stagingDir, relPath);
@@ -456,6 +466,91 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
456
466
  }
457
467
  }
458
468
 
469
+ // ── Plugin-cache → data-dir code sync ──────────────────────
470
+ // Root cause this fixes: a plugin-mode install carries TWO independently
471
+ // versioned code copies sharing one DB. The plugin cache
472
+ // (~/.claude/plugins/cache/<mp>/claude-mem-lite/<ver>/) runs the MCP server and
473
+ // is advanced by Claude Code's marketplace updater; on launch it opens the
474
+ // shared DB and migrates the schema FORWARD. The data-dir copy
475
+ // (~/.claude-mem-lite/) backs the standalone CLI symlink and the settings.json
476
+ // hooks, but is only advanced by the GitHub-tarball auto-update — which plugin
477
+ // mode disables (allowInstall=false) and which stalls easily (24h throttle,
478
+ // rate limits, staging npm install). The data-dir code then lags the schema the
479
+ // cache wrote and the CLI/hooks fail to open the DB
480
+ // ("schema is vN but binary supports up to vN-1").
481
+ //
482
+ // Fix: make the data-dir code TRACK the plugin-cache version. The exact files
483
+ // are already on disk in the cache, so this is a local source-file copy — no
484
+ // network, no npm install — and the synced code is precisely the version that
485
+ // migrated the DB, so schema compatibility is guaranteed by construction.
486
+ // node_modules is left untouched (skipNpmInstall). Only ever upgrades; equal
487
+ // versions no-op, which is the natural per-session throttle.
488
+ //
489
+ // opts.sourceDir — explicit source (launch.mjs passes the running ROOT, the
490
+ // exact version that owns the migrated DB). Omitted → scan
491
+ // the plugin cache for the highest valid version.
492
+ // opts.targetDir — defaults to INSTALL_DIR (the homedir code dir, NOT
493
+ // CLAUDE_MEM_DIR — see schema.mjs CODE_DIR / #8632).
494
+ // opts.cacheBase — override the cache root (tests).
495
+ export async function syncDataDirFromCache(opts = {}) {
496
+ try {
497
+ const targetDir = opts.targetDir || INSTALL_DIR;
498
+
499
+ // Dev install: the data-dir entries are symlinks into the source repo.
500
+ // Overwriting them would clobber the working tree — never sync.
501
+ if (isDevMode()) return { synced: false, reason: 'dev-mode' };
502
+
503
+ let sourceDir = opts.sourceDir || null;
504
+ if (!sourceDir) {
505
+ const cacheBase = opts.cacheBase
506
+ || join(homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'claude-mem-lite');
507
+ if (!existsSync(cacheBase)) return { synced: false, reason: 'no-cache' };
508
+ const versions = readdirSync(cacheBase)
509
+ .filter(n => /^\d+\.\d+/.test(n))
510
+ .sort((a, b) => compareVersions(b, a)); // newest first
511
+ for (const v of versions) {
512
+ const dir = join(cacheBase, v);
513
+ if (validateExtractedTarball(dir, null).ok) { sourceDir = dir; break; }
514
+ }
515
+ if (!sourceDir) return { synced: false, reason: 'no-valid-cache-version' };
516
+ }
517
+
518
+ // Non-plugin direct install: ROOT === data dir. Syncing a dir onto itself
519
+ // is a no-op at best and a same-path rename hazard at worst.
520
+ if (resolve(sourceDir) === resolve(targetDir)) {
521
+ return { synced: false, reason: 'source-is-target' };
522
+ }
523
+
524
+ const val = validateExtractedTarball(sourceDir, null);
525
+ if (!val.ok) return { synced: false, reason: `invalid-source: ${val.reason}` };
526
+
527
+ let sourceVersion;
528
+ try {
529
+ sourceVersion = JSON.parse(readFileSync(join(sourceDir, 'package.json'), 'utf8')).version;
530
+ } catch { return { synced: false, reason: 'source-version-unreadable' }; }
531
+
532
+ let dataVersion = '0.0.0';
533
+ try {
534
+ dataVersion = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf8')).version || '0.0.0';
535
+ } catch { /* missing/corrupt target package.json → treat as 0.0.0, sync */ }
536
+
537
+ // Only ever upgrade. Equal → no-op (cheap version compare runs every session).
538
+ if (compareVersions(sourceVersion, dataVersion) <= 0) {
539
+ return { synced: false, reason: 'data-dir-current', sourceVersion, dataVersion };
540
+ }
541
+
542
+ debugLog('DEBUG', 'hook-update',
543
+ `Syncing data-dir code ${dataVersion} → ${sourceVersion} from plugin cache (${sourceDir})`);
544
+ const ok = await installExtractedRelease(sourceDir, targetDir, { skipNpmInstall: true });
545
+ return ok
546
+ ? { synced: true, from: dataVersion, to: sourceVersion }
547
+ : { synced: false, reason: 'install-failed', from: dataVersion, to: sourceVersion };
548
+ } catch (err) {
549
+ debugCatch(err, 'syncDataDirFromCache');
550
+ return { synced: false, reason: 'error' };
551
+ }
552
+ }
553
+
459
554
  function copyReleaseIntoStaging(sourceDir, stagingDir, manifest = { SOURCE_FILES: LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES: LOCAL_HOOK_SCRIPT_FILES }) {
460
555
  let copied = 0;
461
556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.93.0",
3
+ "version": "2.95.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",
@@ -104,6 +104,31 @@ async function attemptHeal(reason) {
104
104
  return result.status === 0;
105
105
  }
106
106
 
107
+ // Defense-in-depth for plugin-mode version drift: the plugin-cache MCP server
108
+ // (kept current by Claude Code) migrates the shared DB schema forward, while
109
+ // this data-dir code (the standalone CLI + these hooks) is only advanced by the
110
+ // GitHub-tarball auto-update, which plugin mode disables — so it can lag the
111
+ // schema and fail to open the DB. syncDataDirFromCache copies the current cache
112
+ // source files locally to close that gap. launch.mjs (MCP start) is the primary
113
+ // healer; this is a backup that also covers the case where the MCP server never
114
+ // starts. Gated to session-start (once per session, OFF the per-tool hot path)
115
+ // and fully best-effort: a stale module without the fn, or any error, just
116
+ // falls through to the normal entry import. The dynamic import keeps this
117
+ // launcher's pure-`node:` static-import charter intact (it must survive a broken
118
+ // install even if hook-update.mjs is unimportable).
119
+ async function trySyncDataDirFromCache() {
120
+ try {
121
+ const { syncDataDirFromCache } = await import(
122
+ pathToFileURL(join(INSTALL_DIR, 'hook-update.mjs')).href
123
+ );
124
+ if (typeof syncDataDirFromCache === 'function') await syncDataDirFromCache();
125
+ } catch { /* best-effort — proceed to the normal entry regardless */ }
126
+ }
127
+
128
+ if (rest.includes('session-start')) {
129
+ await trySyncDataDirFromCache();
130
+ }
131
+
107
132
  try {
108
133
  await runEntry();
109
134
  } catch (e) {
@@ -74,6 +74,27 @@ try {
74
74
  }
75
75
  }
76
76
 
77
+ // Keep the data-dir code (~/.claude-mem-lite/ — backs the standalone CLI symlink
78
+ // and the settings.json hooks) in lockstep with THIS running version. In plugin
79
+ // mode the MCP server runs from the plugin cache (kept current by Claude Code's
80
+ // marketplace updater) and migrates the shared DB schema forward; the data-dir
81
+ // copy is only advanced by the GitHub-tarball auto-update, which plugin mode
82
+ // disables and which stalls easily, so it drifts behind and the CLI/hooks then
83
+ // fail to open the DB the cache migrated ("schema is vN but binary supports up
84
+ // to vN-1"). syncDataDirFromCache copies the source files locally (no network,
85
+ // no npm install) so the data-dir becomes exactly the version that owns the DB.
86
+ // Best-effort — a sync failure must never block the MCP server launch. It runs
87
+ // from the current cache code, so an already-drifted install self-heals on the
88
+ // next launch once its cache reaches a version carrying this call.
89
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
90
+ try {
91
+ const { syncDataDirFromCache } = await import('../hook-update.mjs');
92
+ await syncDataDirFromCache({ sourceDir: ROOT });
93
+ } catch (e) {
94
+ process.stderr.write(`[claude-mem-lite] data-dir sync skipped: ${e.message}\n`);
95
+ }
96
+ }
97
+
77
98
  // Dev mode: prefer ~/.claude-mem-lite/server.mjs (symlinked to source) over
78
99
  // CLAUDE_PLUGIN_ROOT (potentially stale plugin cache). This ensures the MCP
79
100
  // server always runs the latest code when installed with `install --dev`.
package/secret-scrub.mjs CHANGED
@@ -61,6 +61,18 @@ export const SECRET_PATTERNS = [
61
61
  [/\bnpm_[a-zA-Z0-9]{36,}\b/g, '***'],
62
62
  // Stripe keys (sk_live_, rk_live_, pk_live_, sk_test_, pk_test_)
63
63
  [/\b[srp]k_(?:live|test)_[a-zA-Z0-9]{20,}\b/g, '***'],
64
+ // SendGrid API keys: SG.<22>.<43> — two dots at fixed offsets make this
65
+ // structurally unmistakable; near-zero false-positive risk.
66
+ [/\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g, '***'],
67
+ // Twilio identifiers: Account SID (AC…) + API Key SID (SK…), each = prefix
68
+ // + exactly 32 hex. The 2-letter prefix + 32-hex shape is specific: an MD5
69
+ // is 32 hex (no AC/SK prefix → no match) and a 40-hex git SHA has no internal
70
+ // \b so the trailing \b can't land mid-string. We deliberately do NOT scrub
71
+ // the bare-hex Twilio *auth token* — see comment block at end re: SHA collision.
72
+ [/\b(?:AC|SK)[0-9a-f]{32}\b/g, '***'],
73
+ // Mailgun private API keys: key-<32 hex>. Prefix-anchored for the same reason;
74
+ // bare 32-hex (no `key-`) is intentionally left alone to avoid hashing FPs.
75
+ [/\bkey-[0-9a-f]{32}\b/g, '***'],
64
76
  // JSON-quoted secrets — error payloads / API responses commonly carry creds
65
77
  // as `{"api_key": "..."}`. The base key=value pattern stops at quotes, so
66
78
  // these slip through. Match the value-quoted form explicitly. Length floor
@@ -69,6 +81,15 @@ export const SECRET_PATTERNS = [
69
81
  // Session cookies in headers / urlencoded bodies (sessionid=, session_id=, JSESSIONID=, PHPSESSID=).
70
82
  // 16+ chars filters out short test fixtures like sessionid=abc.
71
83
  [/\b((?:session[_-]?id|sessionid|jsessionid|phpsessid)\s*[=:]\s*)[^\s,;'"}\]]{16,}/gi, '$1***'],
84
+ // ── DELIBERATELY NOT COVERED: bare high-entropy / "raw N-char" tokens ──────
85
+ // A generic `[A-Fa-f0-9]{40}` / high-entropy regex would scrub this repo's own
86
+ // legitimate data: 40-hex git SHAs, 32-hex MD5s, 64-hex SHA256s, and stored
87
+ // `minhash_sig` values. In a hash-heavy codebase the false-positive cost
88
+ // (silent `***` over real content, lost recall) exceeds the marginal catch —
89
+ // and an entropy gate doesn't help because git SHAs are themselves high-entropy.
90
+ // The contextual forms (token=…, Authorization: Bearer …, "api_key":"…") above
91
+ // already cover the dangerous *labelled* shapes. If you are tempted to add a
92
+ // bare-token pattern here: don't — anchor it to a provider prefix instead.
72
93
  ];
73
94
 
74
95
  /**
package/synonyms.mjs CHANGED
@@ -265,6 +265,13 @@ export const CJK_COMPOUNDS = new Set([
265
265
  // architecture
266
266
  '架构', '设计', '方案', '规划', '文档', '注释', '版本', '分支', '依赖',
267
267
  '性能', '安全', '漏洞', '补丁', '系统', '算法',
268
+ // common task/dev vocab — mined from the zero-dict-keyword prompt slice
269
+ // (benchmark/cjk-straddle-prevalence.mjs). These ubiquitous words were absent
270
+ // from the dictionary, so ~15% of real CJK queries fell through to all-bigram
271
+ // noise. Adding real words is monotonically safe: greedy longest-match only
272
+ // improves, and real compounds cannot create boundary-straddle bigrams.
273
+ '工作', '用户', '完成', '计划', '命令', '工具', '插件', '实施', '处理',
274
+ '清理', '显示', '本地', '改动', '确认', '直接', '开始',
268
275
  ]);
269
276
 
270
277
  // ─── Dispatch Synonyms (unidirectional, broader groupings) ──────────────────