claude-mem-lite 3.18.0 → 3.20.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.18.0",
13
+ "version": "3.20.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.18.0",
3
+ "version": "3.20.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-optimize.mjs CHANGED
@@ -467,6 +467,21 @@ Return ONLY valid JSON:
467
467
  lesson_learned: lessonLearned,
468
468
  });
469
469
  db.transaction(() => {
470
+ // Snapshot the keeper's pre-merge row BEFORE overwriting it, so its original
471
+ // full text survives as a recoverable compressed_into child (mirroring
472
+ // compressGroup / recoverChildrenOf). The keeper is the cluster's most-
473
+ // important member; an in-place overwrite by the LLM's ≤800-char summary
474
+ // would otherwise destroy its original text irreversibly (HIGH-3 data loss).
475
+ // Column list is derived from the live schema (minus id/compressed_into) so
476
+ // it stays correct as migrations add columns; names are internal identifiers.
477
+ const snapCols = db.prepare(`PRAGMA table_info(observations)`).all()
478
+ .map(c => c.name).filter(c => c !== 'id' && c !== 'compressed_into');
479
+ const snapColList = snapCols.join(', ');
480
+ db.prepare(
481
+ `INSERT INTO observations (${snapColList}, compressed_into)
482
+ SELECT ${snapColList}, ? FROM observations WHERE id = ?`
483
+ ).run(keeper.id, keeper.id);
484
+
470
485
  db.prepare(`
471
486
  UPDATE observations SET title=?, narrative=?, concepts=?, facts=?, text=?,
472
487
  importance=?, lesson_learned=?, minhash_sig=?, optimized_at=?
package/hook-update.mjs CHANGED
@@ -387,18 +387,20 @@ export function validateExtractedTarball(sourceDir, expectedVersion, expectedNam
387
387
  }
388
388
 
389
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 = '';
390
+ // Embedded Ed25519 PUBLIC key (SPKI PEM). ACTIVE since v3.20.0 auto-update now
391
+ // FAILS CLOSED: a release missing valid signature assets is refused (the matching
392
+ // private key is the GitHub Actions secret RELEASE_SIGNING_KEY; signer:
393
+ // scripts/sign-release.mjs; verifier core: lib/release-digest.mjs). The signature
394
+ // over v3.19.0's published manifest was verified against this key end-to-end
395
+ // before activation. The CLAUDE_MEM_SKIP_SIG_VERIFY env escape hatch still forces a
396
+ // skip. To ROTATE: generate a new keypair, set the new private key as the secret
397
+ // and ship one signed release with it BEFORE replacing the key below — embedding a
398
+ // key whose releases are not yet signed bricks auto-update (fail-closed on unsigned).
399
+ // Setting this back to '' reverts to opportunistic (install-unsigned) behavior.
400
+ const RELEASE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
401
+ MCowBQYDK2VwAyEAau5x65mqsYxJU2cO2ORteueK71EuB4aphVZds8FOZKk=
402
+ -----END PUBLIC KEY-----
403
+ `;
402
404
  const MANIFEST_ASSET_NAME = 'release-manifest.json';
403
405
  const SIGNATURE_ASSET_NAME = 'release-manifest.json.sig';
404
406
 
package/hook.mjs CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  } from './lib/citation-tracker.mjs';
59
59
  import { extractTailAssistantText, extractStructuredSummary } from './lib/summary-extractor.mjs';
60
60
  import { searchRelevantMemories, formatMemoryLine } from './hook-memory.mjs';
61
- import { recordSkillAdoption } from './registry-recommend.mjs';
61
+ import { recordSkillAdoption, gcOldShadowShards } from './registry-recommend.mjs';
62
62
  import { detectMemOverride } from './lib/mem-override.mjs';
63
63
  import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
64
64
  import { checkForUpdate, getCachedUpdateBanner, isUpdateCheckDue } from './hook-update.mjs';
@@ -1054,6 +1054,8 @@ async function handleSessionStart() {
1054
1054
  // GC stale per-session cooldown files. Cheap (<5ms typical) and idempotent;
1055
1055
  // moved here from pre-tool-recall.js's hot path.
1056
1056
  gcStalePreRecallCooldowns();
1057
+ // Bound the shadow-recommendation log (daily JSONL shards, no GC at write time).
1058
+ try { gcOldShadowShards(); } catch { /* best-effort, never blocks SessionStart */ }
1057
1059
 
1058
1060
  // Plugin cache self-heal: Claude Code auto-updates the marketplace plugin can
1059
1061
  // re-populate cache/<ver>/hooks/hooks.json, reintroducing duplicate hook
package/lib/activity.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  // so they don't pollute the L1 system-prompt memory section.
6
6
 
7
7
  import { sanitizeFtsQuery } from '../utils.mjs';
8
+ import { scrubRecord } from './scrub-record.mjs';
8
9
 
9
10
  /**
10
11
  * Canonical event_type enum — mirrors the events.event_type CHECK constraint.
@@ -48,14 +49,19 @@ export function saveEvent(db, {
48
49
  importance = 1,
49
50
  created_at_epoch = Date.now(),
50
51
  }) {
52
+ // Scrub secrets at the single write choke-point so BOTH the auto-capture
53
+ // (persistHaikuSummary event branch, raw Haiku output) and the CLI /bug,/lesson
54
+ // paths are covered — title/body otherwise land verbatim and are FTS-indexed,
55
+ // searchable, and exportable (HIGH-2 at-rest leak).
56
+ const safe = scrubRecord('events', { title, body });
51
57
  const info = db.prepare(`
52
58
  INSERT INTO events (project, event_type, title, body, file_paths, git_sha, importance, created_at_epoch)
53
59
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
54
60
  `).run(
55
61
  project,
56
62
  event_type,
57
- title,
58
- body,
63
+ safe.title,
64
+ safe.body,
59
65
  file_paths ? JSON.stringify(file_paths) : null,
60
66
  git_sha,
61
67
  importance,
@@ -19,6 +19,11 @@ export const TEXT_FIELDS_BY_TABLE = {
19
19
  'title', 'subtitle', 'text', 'narrative',
20
20
  'concepts', 'facts', 'lesson_learned', 'search_aliases',
21
21
  ],
22
+ // events: the auto-captured bugfix/lesson/decision path (saveEvent) and the
23
+ // CLI /bug + /lesson commands both land here. title/body carry LLM output and
24
+ // user-pasted repro text verbatim, so they must scrub like observations do —
25
+ // event_type/project/git_sha are enums/identifiers/hash, left untouched.
26
+ events: ['title', 'body'],
22
27
  session_summaries: [
23
28
  'request', 'investigated', 'learned',
24
29
  'completed', 'next_steps', 'remaining_items', 'notes',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.18.0",
3
+ "version": "3.20.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",
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Phase-1 invariant: shadow AND live only LOG. Neither emits to stdout nor writes
5
5
  // invocations/recommend_count. Live injection is Phase 2. `off` skips all work.
6
- import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync } from 'fs';
6
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
@@ -109,6 +109,31 @@ function appendShadow(row) {
109
109
  } catch { /* shadow sink must never crash the hook */ }
110
110
  }
111
111
 
112
+ /**
113
+ * Prune shadow-log daily shards older than `retainDays`. appendShadow writes one
114
+ * YYYY-MM-DD.jsonl per day with no GC, so a long-lived install grows the dir
115
+ * unbounded (audit: shadow log non-bounded). Shard date is read from the filename
116
+ * (ISO dates sort lexicographically = chronologically). 90d keeps a full quarter
117
+ * for recommend-stats --days while bounding the dir to ~90 sub-MB files.
118
+ * Best-effort, never throws — called from the SessionStart GC sweep.
119
+ * @returns {number} shards removed
120
+ */
121
+ export function gcOldShadowShards(retainDays = 90) {
122
+ try {
123
+ const dir = shadowDir();
124
+ if (!existsSync(dir)) return 0;
125
+ const cutoff = new Date(Date.now() - retainDays * 86_400_000).toISOString().slice(0, 10);
126
+ let removed = 0;
127
+ for (const name of readdirSync(dir)) {
128
+ const m = /^(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
129
+ if (m && m[1] < cutoff) {
130
+ try { unlinkSync(join(dir, name)); removed++; } catch { /* per-entry, silent */ }
131
+ }
132
+ }
133
+ return removed;
134
+ } catch { return 0; }
135
+ }
136
+
112
137
  export function logShadowReco(project, rec) { appendShadow({ ts: new Date().toISOString(), kind: 'reco', project, ...rec }); }
113
138
  export function logShadowAdoption(project, rec) { appendShadow({ ts: new Date().toISOString(), kind: 'adopt', project, ...rec }); }
114
139
 
package/registry.mjs CHANGED
@@ -165,7 +165,10 @@ export function ensureRegistryDb(dbPath) {
165
165
 
166
166
  const db = new Database(dbPath);
167
167
  db.pragma('journal_mode = WAL');
168
- db.pragma('busy_timeout = 3000');
168
+ // 5000ms to match the main DB: registry writes (install indexing rewriting
169
+ // resources + resources_fts) race shadow-recommend writes + mem_registry reads
170
+ // on the same file; 3000ms was insufficient under that concurrency (schema.mjs).
171
+ db.pragma('busy_timeout = 5000');
169
172
  db.pragma('synchronous = NORMAL');
170
173
  db.pragma('foreign_keys = ON');
171
174
 
package/schema.mjs CHANGED
@@ -918,7 +918,7 @@ export function ensureDb() {
918
918
  * @returns {{rebuilt: string[], errors: string[]}} Results per FTS table
919
919
  */
920
920
  export function rebuildFTS(db) {
921
- const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts'];
921
+ const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts', 'events_fts'];
922
922
  const idRe = /^[a-z][a-z0-9_]*$/;
923
923
  const rebuilt = [];
924
924
  const errors = [];
@@ -942,7 +942,7 @@ export function rebuildFTS(db) {
942
942
  * @returns {{healthy: boolean, details: string[]}}
943
943
  */
944
944
  export function checkFTSIntegrity(db) {
945
- const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts'];
945
+ const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts', 'events_fts'];
946
946
  const idRe = /^[a-z][a-z0-9_]*$/;
947
947
  const details = [];
948
948
  let healthy = true;
package/secret-scrub.mjs CHANGED
@@ -60,8 +60,10 @@ export const SECRET_PATTERNS = [
60
60
  // (b) structured keys + named env vars are unambiguous config even after a word
61
61
  // (`see api_key: "x"` DOES scrub, mirroring the unquoted structured-key path):
62
62
  [/((?:\b|_)(?:pgpassword|pgpass|mysql_pwd|api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|access[_-]?token|refresh[_-]?token)\s*[=:]\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
63
- // AWS access keys (AKIA...)
64
- [/\bAKIA[A-Z0-9]{16}\b/g, '***'],
63
+ // AWS access keys: AKIA (long-term) + ASIA (STS temp) + AROA (role) + AIDA
64
+ // (user) + ANPA/ANVA/AGPA (other principal types). All share the 4-letter
65
+ // prefix + exactly 16 base32 chars shape — specific enough for near-zero FP.
66
+ [/\b(?:AKIA|ASIA|AROA|AIDA|ANPA|ANVA|AGPA)[A-Z0-9]{16}\b/g, '***'],
65
67
  // OpenAI / Anthropic keys (sk-...) — specific prefixes have lower length threshold
66
68
  [/\bsk-(?:proj|ant|ant-api\d{2})-[a-zA-Z0-9_-]{8,}\b/g, '***'],
67
69
  [/\bsk-[a-zA-Z0-9_-]{20,}\b/g, '***'],
@@ -74,8 +76,10 @@ export const SECRET_PATTERNS = [
74
76
  [/\bxox[bpas]-[a-zA-Z0-9-]{10,}\b/g, '***'],
75
77
  // JWT tokens (eyJ...eyJ...)
76
78
  [/\beyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]+\b/g, '***'],
77
- // PEM private key blocks
78
- [/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, '***PEM_KEY***'],
79
+ // PEM private key blocks. `[A-Z0-9 ]*` covers every armor label — RSA/EC/DSA/
80
+ // OPENSSH plus ENCRYPTED and PGP (… PRIVATE KEY BLOCK) that the fixed
81
+ // alternation missed; the block delimiters make FP impossible.
82
+ [/-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g, '***PEM_KEY***'],
79
83
  // Long hex strings in assignments (e.g. SECRET_KEY=abc123def456...)
80
84
  [/(\b(?:key|secret|token|hash)\s*[=:]\s*)[0-9a-f]{32,}\b/gi, '$1***'],
81
85
  // Google Cloud API keys (AIza...)
@@ -84,8 +88,9 @@ export const SECRET_PATTERNS = [
84
88
  [/(Authorization:\s*Bearer\s+)[^\s,;'"}\]]+/gi, '$1***'],
85
89
  // Supabase / generic long base64 keys (40+ chars, common in env vars)
86
90
  [/(\b(?:SUPABASE_KEY|SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*)[^\s,;'"}\]]+/gi, '$1***'],
87
- // Basic auth in URLs (https://user:password@host)
88
- [/https?:\/\/[^@/\s]+:[^@/\s]+@/gi, 'https://***:***@'],
91
+ // Basic auth in URLs (https://user:password@host). ftp/ftps added — file-drop
92
+ // creds are a common leak shape the https-only form missed.
93
+ [/(https?|ftps?):\/\/[^@/\s]+:[^@/\s]+@/gi, '$1://***:***@'],
89
94
  // Database connection strings (postgres, mysql, mongodb, redis, amqp)
90
95
  [/\b(postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s,;'"}\]]+/gi, '$1://***'],
91
96
  // npm tokens (npm_...)
@@ -109,6 +114,13 @@ export const SECRET_PATTERNS = [
109
114
  // these slip through. Match the value-quoted form explicitly. Length floor
110
115
  // (6) avoids tripping on intentional placeholder shorts ("...", "secret").
111
116
  [/("(?:password|passwd|token|api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer|refresh[_-]?token|session[_-]?id|sessionid)"\s*:\s*")[^"]{6,}(")/gi, '$1***$2'],
117
+ // JSON keys with vendor PREFIX/SUFFIX around the core credential noun —
118
+ // `"x_api_key"`, `"aws_secret_access_key"`, `"my_password"`, `"gh_token"`.
119
+ // The exact-name list above misses these. Anchored to the credential nouns
120
+ // (password|secret|api_key|auth_token|access_token|private_key) so a benign
121
+ // `"token_count"` value (numeric, <6 non-quote chars after scrub) and prose
122
+ // keys stay low-FP; over-scrub is the safe direction for at-rest memory.
123
+ [/("\w*(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*"\s*:\s*")[^"]{6,}(")/gi, '$1***$2'],
112
124
  // Session cookies in headers / urlencoded bodies (sessionid=, session_id=, JSESSIONID=, PHPSESSID=).
113
125
  // 16+ chars filters out short test fixtures like sessionid=abc.
114
126
  [/\b((?:session[_-]?id|sessionid|jsessionid|phpsessid)\s*[=:]\s*)[^\s,;'"}\]]{16,}/gi, '$1***'],
package/server.mjs CHANGED
@@ -98,7 +98,7 @@ function getRegistryDb() {
98
98
  if (registryDb) return registryDb;
99
99
  try {
100
100
  registryDb = ensureRegistryDb(REGISTRY_DB_PATH);
101
- registryDb.pragma('busy_timeout = 3000');
101
+ registryDb.pragma('busy_timeout = 5000'); // match main DB + ensureRegistryDb (was 3000, overriding it back down)
102
102
  } catch (e) {
103
103
  debugLog('WARN', 'server', `Registry DB not available: ${e.message}`);
104
104
  }