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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/README.zh-CN.md +1 -1
- package/hook-update.mjs +102 -2
- package/hook.mjs +403 -373
- package/install.mjs +666 -629
- package/lib/doctor-benchmark.mjs +4 -4
- package/lib/release-digest.mjs +106 -0
- package/lib/search-core.mjs +272 -16
- package/mem-cli.mjs +55 -174
- package/package.json +3 -2
- package/schema.mjs +7 -1
- package/scripts/setup.sh +2 -0
- package/search-engine.mjs +1 -1
- package/{server-internals.mjs → search-scoring.mjs} +6 -2
- package/server.mjs +72 -293
- package/source-files.mjs +5 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|