claude-mem-lite 3.7.0 → 3.8.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": "3.7.0",
13
+ "version": "3.8.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.7.0",
3
+ "version": "3.8.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
@@ -172,6 +172,8 @@ npx github:sdsrss/claude-mem-lite
172
172
 
173
173
  Source files are automatically copied to `~/.claude-mem-lite/` for persistence.
174
174
 
175
+ > **Note:** `npx github:…` installs from the repo's **default branch (HEAD)**, which can be ahead of the latest published release. For the stable released version use the npm package (`npx claude-mem-lite`), or pin a release tag: `npx github:sdsrss/claude-mem-lite#vX.Y.Z`.
176
+
175
177
  ### Method 3: git clone
176
178
 
177
179
  ```bash
@@ -582,7 +584,7 @@ claude-mem-lite/
582
584
  commands/
583
585
  mem.md # /mem command definition
584
586
  server.mjs # MCP server: tool definitions, FTS5 search, database init
585
- server-internals.mjs # Extracted search helpers: re-ranking, PRF, concept expansion
587
+ search-scoring.mjs # Extracted search helpers: re-ranking, PRF, concept expansion
586
588
  hook.mjs # Claude Code hooks: episode capture, error recall, session management
587
589
  hook-llm.mjs # Background LLM workers: episode extraction, session summaries
588
590
  hook-shared.mjs # Shared hook infrastructure: session management, DB access, LLM calls
package/README.zh-CN.md CHANGED
@@ -524,7 +524,7 @@ claude-mem-lite/
524
524
  commands/
525
525
  mem.md # /mem 命令定义
526
526
  server.mjs # MCP 服务器:工具定义、FTS5 搜索、数据库初始化
527
- server-internals.mjs # 搜索辅助模块:重排序、PRF、概念扩展
527
+ search-scoring.mjs # 搜索辅助模块:重排序、PRF、概念扩展
528
528
  hook.mjs # Claude Code 钩子:episode 捕获、错误回忆、会话管理
529
529
  hook-llm.mjs # 后台 LLM worker:episode 提取、会话摘要
530
530
  hook-shared.mjs # 共享钩子基础设施:会话管理、数据库访问、LLM 调用
package/hook-update.mjs CHANGED
@@ -15,6 +15,7 @@ import { debugCatch, debugLog } from './utils.mjs';
15
15
  import { SOURCE_FILES as LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES as LOCAL_HOOK_SCRIPT_FILES } from './source-files.mjs';
16
16
  import { acquireLock } from './lib/proc-lock.mjs';
17
17
  import { atomicWriteFileSync } from './lib/atomic-write.mjs';
18
+ import { verifyReleaseFiles, verifyManifestSignature } from './lib/release-digest.mjs';
18
19
 
19
20
  // ── Configuration ──────────────────────────────────────────
20
21
  const GITHUB_REPO = 'sdsrss/claude-mem-lite';
@@ -77,7 +78,7 @@ export async function checkForUpdate(options = {}) {
77
78
  if (hasUpdate) {
78
79
  debugLog('DEBUG', 'hook-update', `Update available: ${currentVersion} → ${latest.version}`);
79
80
  const canInstall = !pluginMode && Boolean(allowInstall);
80
- const success = canInstall ? await downloadAndInstall(latest.tarballUrl, latest.version) : false;
81
+ const success = canInstall ? await downloadAndInstall(latest.tarballUrl, latest.version, latest.assets) : false;
81
82
  const newState = {
82
83
  lastCheck: new Date().toISOString(),
83
84
  installedVersion: success ? latest.version : currentVersion,
@@ -206,6 +207,7 @@ async function fetchLatestRelease() {
206
207
  version: result.tag_name.replace(/^v/, ''),
207
208
  tarballUrl: result.tarball_url,
208
209
  releaseUrl: result.html_url,
210
+ assets: Array.isArray(result.assets) ? result.assets : [],
209
211
  };
210
212
  }
211
213
 
@@ -221,6 +223,7 @@ async function fetchLatestRelease() {
221
223
  version: tag.name.replace(/^v/, ''),
222
224
  tarballUrl: `https://api.github.com/repos/${GITHUB_REPO}/tarball/${tag.name}`,
223
225
  releaseUrl: `https://github.com/${GITHUB_REPO}/releases/tag/${tag.name}`,
226
+ assets: [],
224
227
  };
225
228
  }
226
229
 
@@ -301,7 +304,7 @@ async function loadReleaseManifest(sourceDir) {
301
304
 
302
305
  // ── Download & Install ─────────────────────────────────────
303
306
  // Direct file copy instead of running old install.mjs (avoids symlink overwrite in dev)
304
- async function downloadAndInstall(tarballUrl, expectedVersion) {
307
+ async function downloadAndInstall(tarballUrl, expectedVersion, assets = []) {
305
308
  const tmpDir = join(tmpdir(), `claude-mem-lite-update-${Date.now()}`);
306
309
  try {
307
310
  mkdirSync(tmpDir, { recursive: true });
@@ -324,6 +327,17 @@ async function downloadAndInstall(tarballUrl, expectedVersion) {
324
327
  return false;
325
328
  }
326
329
 
330
+ // P1 supply-chain: cryptographically verify the release before installing.
331
+ // Opportunistic + inert until keyed — ok=false ONLY on a real tampering
332
+ // signal (signature present but invalid, or a file hash mismatch). Missing
333
+ // key / missing signature assets / fetch failure / escape hatch all proceed,
334
+ // so this never bricks auto-update for unsigned or pre-key releases.
335
+ const authentic = await verifyReleaseAuthenticity(tmpDir, assets);
336
+ if (!authentic.ok) {
337
+ debugLog('WARN', 'hook-update', `Release authenticity check failed (${authentic.action}) — aborting update`);
338
+ return false;
339
+ }
340
+
327
341
  return await installExtractedRelease(tmpDir);
328
342
  } catch (err) {
329
343
  debugCatch(err, 'downloadAndInstall');
@@ -372,6 +386,92 @@ export function validateExtractedTarball(sourceDir, expectedVersion, expectedNam
372
386
  return { ok: true };
373
387
  }
374
388
 
389
+ // ── Release signature verification (P1 supply-chain hardening) ──────────────
390
+ // Embedded Ed25519 PUBLIC key (SPKI PEM). EMPTY = unconfigured → verification is
391
+ // INERT and auto-update behaves exactly as before. Activating it is a one-time
392
+ // ops step (no private key ever ships in the repo):
393
+ // 1. Generate a keypair (writes the PRIVATE key to a local file, prints PUBLIC):
394
+ // node -e "const c=require('crypto');const{publicKey,privateKey}=c.generateKeyPairSync('ed25519');process.stdout.write(publicKey.export({type:'spki',format:'pem'}));require('fs').writeFileSync('release-signing-key.pem',privateKey.export({type:'pkcs8',format:'pem'}))"
395
+ // 2. Paste the printed PUBLIC key between the backticks below.
396
+ // 3. Add the PRIVATE key (release-signing-key.pem contents) as the GitHub
397
+ // Actions secret RELEASE_SIGNING_KEY, then delete the local file.
398
+ // NEVER commit the private key.
399
+ // Once a signed release exists, clients verify it; unsigned/older releases still
400
+ // install (opportunistic). Signer: scripts/sign-release.mjs. Core: lib/release-digest.mjs.
401
+ const RELEASE_PUBLIC_KEY = '';
402
+ const MANIFEST_ASSET_NAME = 'release-manifest.json';
403
+ const SIGNATURE_ASSET_NAME = 'release-manifest.json.sig';
404
+
405
+ // Pure verifier (no I/O) — exported for unit testing. ok=true ONLY when the
406
+ // Ed25519 signature over `manifestBytes` is valid for `publicKeyPem` AND every
407
+ // file the manifest lists matches its sha256 under `extractedDir`.
408
+ export function verifyDownloadedRelease(extractedDir, manifestBytes, signatureB64, publicKeyPem = RELEASE_PUBLIC_KEY) {
409
+ if (!verifyManifestSignature(manifestBytes, signatureB64, publicKeyPem)) {
410
+ return { ok: false, reason: 'signature-invalid' };
411
+ }
412
+ let manifest;
413
+ try {
414
+ manifest = JSON.parse(Buffer.isBuffer(manifestBytes) ? manifestBytes.toString('utf8') : String(manifestBytes));
415
+ } catch {
416
+ return { ok: false, reason: 'manifest-unparseable' };
417
+ }
418
+ const files = verifyReleaseFiles(extractedDir, manifest);
419
+ if (!files.ok) {
420
+ return { ok: false, reason: `file-mismatch: ${[...files.mismatches, ...files.missing].slice(0, 5).join(', ')}` };
421
+ }
422
+ return { ok: true, reason: 'verified' };
423
+ }
424
+
425
+ // Fetch a GitHub Release asset as a Buffer. Host-locked to github.com (the asset
426
+ // browser_download_url); GitHub's own 302 to its CDN is followed by fetch.
427
+ async function fetchAssetBuffer(url) {
428
+ if (!/^https:\/\/github\.com\/[\w./%~-]+$/.test(url || '')) {
429
+ throw new Error(`rejected asset url: ${url}`);
430
+ }
431
+ const controller = new AbortController();
432
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
433
+ try {
434
+ const res = await fetch(url, { signal: controller.signal, redirect: 'follow' });
435
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
436
+ return Buffer.from(await res.arrayBuffer());
437
+ } finally {
438
+ clearTimeout(timeout);
439
+ }
440
+ }
441
+
442
+ // I/O gate called from downloadAndInstall after validateExtractedTarball.
443
+ // Opportunistic: returns ok=false ONLY on a genuine tampering signal. Missing
444
+ // embedded key, missing signature assets, asset-fetch failure, or the
445
+ // CLAUDE_MEM_SKIP_SIG_VERIFY escape hatch all return ok=true so a verification
446
+ // gap can never permanently brick auto-update.
447
+ export async function verifyReleaseAuthenticity(extractedDir, assets) {
448
+ if (process.env.CLAUDE_MEM_SKIP_SIG_VERIFY) return { ok: true, action: 'skipped-env' };
449
+ if (!RELEASE_PUBLIC_KEY) return { ok: true, action: 'skipped-no-pubkey' };
450
+
451
+ const list = Array.isArray(assets) ? assets : [];
452
+ const manifestAsset = list.find(a => a && a.name === MANIFEST_ASSET_NAME);
453
+ const sigAsset = list.find(a => a && a.name === SIGNATURE_ASSET_NAME);
454
+ if (!manifestAsset || !sigAsset) {
455
+ debugLog('WARN', 'hook-update', 'Release carries no signature assets — proceeding unverified (unsigned release)');
456
+ return { ok: true, action: 'skipped-no-signature' };
457
+ }
458
+
459
+ let manifestBytes, signatureB64;
460
+ try {
461
+ manifestBytes = await fetchAssetBuffer(manifestAsset.browser_download_url);
462
+ signatureB64 = (await fetchAssetBuffer(sigAsset.browser_download_url)).toString('utf8').trim();
463
+ } catch (e) {
464
+ // A flaky asset CDN is not a tampering signal — don't brick the update over it.
465
+ debugLog('WARN', 'hook-update', `Signature asset fetch failed (${e.message}) — proceeding unverified`);
466
+ return { ok: true, action: 'skipped-fetch-failed' };
467
+ }
468
+
469
+ const r = verifyDownloadedRelease(extractedDir, manifestBytes, signatureB64);
470
+ if (!r.ok) return { ok: false, action: r.reason };
471
+ debugLog('DEBUG', 'hook-update', 'Release signature verified');
472
+ return { ok: true, action: 'verified' };
473
+ }
474
+
375
475
  // opts.skipNpmInstall — copy + atomically switch the source files WITHOUT
376
476
  // running `npm install` in staging. Used by syncDataDirFromCache: when the
377
477
  // source is a local plugin-cache version (not a downloaded tarball), the