claude-mem-lite 3.7.1 → 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 +2 -0
- package/hook-update.mjs +102 -2
- package/lib/release-digest.mjs +106 -0
- package/package.json +2 -1
- package/scripts/setup.sh +2 -0
- package/source-files.mjs +4 -0
|
@@ -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
|
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
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// lib/release-digest.mjs — shared release-signing core (P1 supply-chain hardening).
|
|
2
|
+
//
|
|
3
|
+
// One source of truth for BOTH sides of the auto-update authenticity check so the
|
|
4
|
+
// CI signer and the runtime verifier can never drift:
|
|
5
|
+
// * scripts/sign-release.mjs (CI) — builds the manifest, serializes it, signs
|
|
6
|
+
// the EXACT serialized bytes with an Ed25519 private key (a GitHub secret),
|
|
7
|
+
// and uploads release-manifest.json (+ .sig) as GitHub Release assets.
|
|
8
|
+
// * hook-update.mjs (client) — downloads those assets, verifies the signature
|
|
9
|
+
// over the downloaded manifest bytes with an EMBEDDED public key, then checks
|
|
10
|
+
// each extracted file's sha256 against the signed manifest.
|
|
11
|
+
//
|
|
12
|
+
// Why content hashes, not a tarball-byte hash: GitHub's on-the-fly git-archive
|
|
13
|
+
// tarballs are not guaranteed byte-stable over time, but the FILE CONTENTS at a
|
|
14
|
+
// tag are (they are the git blobs). Hashing the extracted files sidesteps the
|
|
15
|
+
// archive-byte-stability problem entirely.
|
|
16
|
+
//
|
|
17
|
+
// Why verify over the downloaded file bytes (not a re-serialization): the client
|
|
18
|
+
// verifies the signature against the manifest bytes exactly as downloaded and
|
|
19
|
+
// only THEN parses it — so there is no canonical-JSON drift between signer and
|
|
20
|
+
// verifier. serializeManifest() exists so CI writes precisely what it signs.
|
|
21
|
+
//
|
|
22
|
+
// Pure Node built-ins (node:crypto Ed25519) — zero dependencies, no `gh`/cosign
|
|
23
|
+
// needed on the user's machine, works in the silent SessionStart hook.
|
|
24
|
+
|
|
25
|
+
import { createHash, verify as cryptoVerify } from 'node:crypto';
|
|
26
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
const PACKAGE_NAME = 'claude-mem-lite';
|
|
30
|
+
|
|
31
|
+
export function sha256Hex(buf) {
|
|
32
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sha256File(absPath) {
|
|
36
|
+
return sha256Hex(readFileSync(absPath));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a release manifest: { name, version, algo, files: { <relPath>: <sha256> } }.
|
|
41
|
+
* Only files that exist under rootDir are listed; keys are sorted so the
|
|
42
|
+
* serialization is deterministic regardless of fileList order.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} rootDir Extracted release root (or CI checkout root)
|
|
45
|
+
* @param {string[]} fileList Relative paths to hash (typically SOURCE_FILES)
|
|
46
|
+
* @param {string} version Release version (for the manifest body)
|
|
47
|
+
*/
|
|
48
|
+
export function buildReleaseManifest(rootDir, fileList, version) {
|
|
49
|
+
const files = {};
|
|
50
|
+
for (const rel of [...fileList].sort()) {
|
|
51
|
+
const abs = join(rootDir, rel);
|
|
52
|
+
if (existsSync(abs)) files[rel] = sha256File(abs);
|
|
53
|
+
}
|
|
54
|
+
return { name: PACKAGE_NAME, version, algo: 'sha256', files };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deterministic byte serialization. CI writes EXACTLY this to
|
|
59
|
+
* release-manifest.json and signs these bytes; the client verifies the signature
|
|
60
|
+
* against the downloaded file bytes (it does not re-serialize), so signer and
|
|
61
|
+
* verifier cannot disagree on canonical form.
|
|
62
|
+
*/
|
|
63
|
+
export function serializeManifest(manifest) {
|
|
64
|
+
return JSON.stringify(manifest, null, 2) + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Verify every file the manifest lists matches its signed sha256 on disk.
|
|
69
|
+
* Returns { ok, mismatches: string[], missing: string[] }.
|
|
70
|
+
* A content change → mismatches; a manifest-listed file absent → missing.
|
|
71
|
+
* Both are failures (ok=false).
|
|
72
|
+
*/
|
|
73
|
+
export function verifyReleaseFiles(rootDir, manifest) {
|
|
74
|
+
const mismatches = [];
|
|
75
|
+
const missing = [];
|
|
76
|
+
const files = (manifest && manifest.files) || {};
|
|
77
|
+
for (const [rel, expected] of Object.entries(files)) {
|
|
78
|
+
const abs = join(rootDir, rel);
|
|
79
|
+
if (!existsSync(abs)) { missing.push(rel); continue; }
|
|
80
|
+
if (sha256File(abs) !== expected) mismatches.push(rel);
|
|
81
|
+
}
|
|
82
|
+
return { ok: mismatches.length === 0 && missing.length === 0, mismatches, missing };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify an Ed25519 signature over the manifest bytes. Never throws — a malformed
|
|
87
|
+
* key/signature/algorithm returns false so callers can treat "can't verify" the
|
|
88
|
+
* same as "invalid" without a try/catch at every call site.
|
|
89
|
+
*
|
|
90
|
+
* @param {Buffer|string} manifestBytes The EXACT manifest bytes that were signed
|
|
91
|
+
* @param {string} signatureB64 Base64-encoded Ed25519 signature
|
|
92
|
+
* @param {string} publicKeyPem SPKI PEM public key
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function verifyManifestSignature(manifestBytes, signatureB64, publicKeyPem) {
|
|
96
|
+
try {
|
|
97
|
+
if (!publicKeyPem || !signatureB64) return false;
|
|
98
|
+
const data = Buffer.isBuffer(manifestBytes) ? manifestBytes : Buffer.from(manifestBytes);
|
|
99
|
+
const sig = Buffer.from(signatureB64, 'base64');
|
|
100
|
+
if (sig.length === 0) return false;
|
|
101
|
+
// `null` algorithm = Ed25519 (the key type carries the algorithm in Node).
|
|
102
|
+
return cryptoVerify(null, data, publicKeyPem, sig);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"lib/binding-probe.mjs",
|
|
74
74
|
"lib/proc-lock.mjs",
|
|
75
75
|
"lib/atomic-write.mjs",
|
|
76
|
+
"lib/release-digest.mjs",
|
|
76
77
|
"lib/mem-override.mjs",
|
|
77
78
|
"lib/save-observation.mjs",
|
|
78
79
|
"lib/observation-write.mjs",
|
package/scripts/setup.sh
CHANGED
|
@@ -86,6 +86,7 @@ mark_deps_broken() {
|
|
|
86
86
|
# having to re-derive them. Delegate JSON serialization to node so embedded
|
|
87
87
|
# quotes / shell metachars in $ROOT or $reason can't produce an invalid file
|
|
88
88
|
# (bash `printf '"..%s.."'` cannot escape arbitrary strings safely; v2.79.1 fix).
|
|
89
|
+
# shellcheck disable=SC2016 # node script single-quoted on purpose; vars passed via env (MARK_*), not shell expansion
|
|
89
90
|
MARK_REASON="$reason" MARK_ROOT="$ROOT" MARK_FLAG="$DEPS_FLAG" node -e '
|
|
90
91
|
const fs = require("fs");
|
|
91
92
|
const reason = process.env.MARK_REASON || "unknown";
|
|
@@ -141,6 +142,7 @@ fi
|
|
|
141
142
|
# versions; same shape as the .deps-broken self-heal pattern.
|
|
142
143
|
MCP_MIGRATION="$DATA_DIR/runtime/.mcp-dedup-v2.78"
|
|
143
144
|
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$MCP_MIGRATION" ]]; then
|
|
145
|
+
# shellcheck disable=SC2016 # node script single-quoted on purpose; CLAUDE_JSON passed via env, not shell expansion
|
|
144
146
|
CLAUDE_JSON="$HOME/.claude.json" node -e '
|
|
145
147
|
const fs = require("fs");
|
|
146
148
|
let changed = false;
|
package/source-files.mjs
CHANGED
|
@@ -73,6 +73,10 @@ export const SOURCE_FILES = [
|
|
|
73
73
|
// + auto-update lock). Must ship or a partial install/update skips them.
|
|
74
74
|
'lib/proc-lock.mjs',
|
|
75
75
|
'lib/atomic-write.mjs',
|
|
76
|
+
// P1 supply-chain: shared release-signing core (sha256 manifest + Ed25519
|
|
77
|
+
// verify). Imported by hook-update.mjs (verify) + scripts/sign-release.mjs (CI
|
|
78
|
+
// sign). Must ship or auto-update can't verify release signatures.
|
|
79
|
+
'lib/release-digest.mjs',
|
|
76
80
|
// v2.41 god-module split — mem-cli.mjs router + per-cmd handlers under cli/
|
|
77
81
|
'cli/common.mjs',
|
|
78
82
|
'cli/fts-check.mjs',
|