claude-mem-lite 2.59.0 → 2.60.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.59.0",
13
+ "version": "2.60.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.59.0",
3
+ "version": "2.60.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'adopt', 'unadopt', 'help']);
2
+ const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'help']);
3
3
  const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'release']);
4
4
 
5
5
  const cmd = process.argv[2];
package/hook-memory.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // claude-mem-lite — Semantic Memory Injection
2
2
  // Search past observations for relevant memories to inject as context at user-prompt time.
3
3
 
4
- import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords } from './utils.mjs';
4
+ import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords } from './utils.mjs';
5
5
  import { recordMetric } from './lib/metrics.mjs';
6
6
  import { DB_DIR } from './schema.mjs';
7
7
 
@@ -78,6 +78,44 @@ function candidateCoverage(row, queryTerms) {
78
78
  const FILE_RECALL_LOOKBACK_MS = 60 * 86400000; // 60 days
79
79
  const MAX_FILE_RECALL = 2;
80
80
 
81
+ // P1: stale-obs verify-before-use threshold. An injected obs older than this
82
+ // AND carrying file paths is flagged so Claude is reminded to grep/Read the
83
+ // referenced code before applying the lesson — code may have moved or been
84
+ // renamed since capture. Pure-decision/architecture obs (no file_paths)
85
+ // don't get the hint: their drift is text-only and Claude already verifies
86
+ // at consumption time per the project mem-usage contract.
87
+ const STALE_OBS_THRESHOLD_MS = 30 * 86400000;
88
+
89
+ /**
90
+ * Format a single line for the <memory-context> block emitted by
91
+ * handleUserPrompt. Pure function — exported for unit testing.
92
+ *
93
+ * @param {object} obs Row with {id, type, title, lesson_learned,
94
+ * created_at_epoch, files_modified}. files_modified is a JSON-encoded
95
+ * string array (column shape) or null.
96
+ * @returns {string} `- [type] title[ | Lesson: X] (#id)[ [verify-before-use]]`
97
+ */
98
+ export function formatMemoryLine(obs) {
99
+ const lessonTag = obs.lesson_learned ? ` | Lesson: ${obs.lesson_learned}` : '';
100
+ let staleHint = '';
101
+ if (typeof obs.created_at_epoch === 'number'
102
+ && Date.now() - obs.created_at_epoch > STALE_OBS_THRESHOLD_MS
103
+ && hasFilePaths(obs.files_modified)) {
104
+ staleHint = ' [verify-before-use]';
105
+ }
106
+ return `- [${obs.type}] ${truncate(obs.title, 80)}${lessonTag} (#${obs.id})${staleHint}`;
107
+ }
108
+
109
+ function hasFilePaths(filesModified) {
110
+ if (!filesModified || typeof filesModified !== 'string') return false;
111
+ try {
112
+ const arr = JSON.parse(filesModified);
113
+ return Array.isArray(arr) && arr.length > 0;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
81
119
  /**
82
120
  * Search for relevant past observations to inject as memory context.
83
121
  * Quality gates: importance>=1 (with 0.6x penalty), type-boosted, lesson-boosted, BM25-thresholded (adaptive: 0 for <5 obs, 1.5 otherwise).
@@ -124,6 +162,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
124
162
  // penalty factor (for the final JS score).
125
163
  const selectStmt = db.prepare(`
126
164
  SELECT o.id, o.type, o.title, o.subtitle, o.narrative, o.importance, o.lesson_learned, o.project,
165
+ o.created_at_epoch, o.files_modified,
127
166
  ${OBS_BM25} as relevance,
128
167
  ${noisePenaltyClause('o')} as noise_penalty
129
168
  FROM observations_fts
package/hook.mjs CHANGED
@@ -45,7 +45,8 @@ import {
45
45
  import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
46
46
  import { extractCitationsFromTranscript, bumpCitationAccess } from './lib/citation-tracker.mjs';
47
47
  import { extractTailAssistantText, extractStructuredSummary } from './lib/summary-extractor.mjs';
48
- import { searchRelevantMemories } from './hook-memory.mjs';
48
+ import { searchRelevantMemories, formatMemoryLine } from './hook-memory.mjs';
49
+ import { detectMemOverride } from './lib/mem-override.mjs';
49
50
  import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
50
51
  import { checkForUpdate } from './hook-update.mjs';
51
52
  import { handleLLMOptimize } from './hook-optimize.mjs';
@@ -1111,8 +1112,11 @@ async function handleUserPrompt() {
1111
1112
  } catch (e) { debugCatch(e, 'handleUserPrompt-handoff'); }
1112
1113
  }
1113
1114
 
1114
- // Semantic memory injection: search past observations for the user's prompt
1115
- try {
1115
+ // Semantic memory injection: search past observations for the user's prompt.
1116
+ // P0 short-circuit on user-explicit "ignore memory" / "不要用记忆" override
1117
+ // (mirrors CC built-in memoryTypes.ts:215). Skip both Key Context lookup
1118
+ // and the <memory-context> emission for this turn.
1119
+ if (!detectMemOverride(promptText)) try {
1116
1120
  const keyObs = db.prepare(`
1117
1121
  SELECT id FROM observations
1118
1122
  WHERE project = ? AND COALESCE(compressed_into, 0) = 0
@@ -1135,10 +1139,7 @@ async function handleUserPrompt() {
1135
1139
  const memories = searchRelevantMemories(db, promptText, project, keyContextIds);
1136
1140
  if (memories.length > 0) {
1137
1141
  const lines = ['<memory-context relevance="high">'];
1138
- for (const m of memories) {
1139
- const lessonTag = m.lesson_learned ? ` | Lesson: ${m.lesson_learned}` : '';
1140
- lines.push(`- [${m.type}] ${truncate(m.title, 80)}${lessonTag} (#${m.id})`);
1141
- }
1142
+ for (const m of memories) lines.push(formatMemoryLine(m));
1142
1143
  lines.push('</memory-context>');
1143
1144
  process.stdout.write(lines.join('\n') + '\n');
1144
1145
  }
@@ -0,0 +1,31 @@
1
+ // User-explicit "ignore memory" override detector. Mirrors CC built-in
2
+ // memoryTypes.ts:215 ("If the user says to *ignore* or *not use* memory:
3
+ // Do not apply remembered facts"). Tight regexes — must require both an
4
+ // "ignore-class" verb AND the memory token, so phrases like "memory leak",
5
+ // "记忆中的事件", "MEM-1234" pass through unaffected.
6
+ //
7
+ // Two parallel patterns:
8
+ // EN — ignore|skip|forget|disable|drop|reject + (optional qualifier)
9
+ // + memor(y|ies) | memory-context | past context | recall;
10
+ // plus the negated form: do not / don't + use|read|inject|apply.
11
+ // CN — 1) ignore-class verbs (无视|忽略|忽视|跳过|拒绝|不再[用|看|读|参考])
12
+ // + (optional qualifier) + 记忆
13
+ // 2) 不要|别|不需|不必 + use-class verb (用|看|读|参考|...) + 记忆.
14
+ //
15
+ // Lives under lib/ (not scripts/) because hook.mjs imports it directly
16
+ // for the handleUserPrompt short-circuit. install.mjs/hook-update.mjs
17
+ // rename scripts/ as a directory; an individual `scripts/<file>.mjs`
18
+ // entry in SOURCE_FILES would collide with that rename.
19
+
20
+ const MEM_OVERRIDE_EN = /\b(?:ignore|skip|forget|disable|drop|reject)\s+(?:(?:any|all|the|past|prior|previous|recalled?|injected|stored)\s+){0,3}(?:memor(?:y|ies)|memory-?context|mem[\s-]context|past\s+context|recall)\b|\b(?:do\s+not|don['’`]?t)\s+(?:use|read|inject|apply)\s+(?:(?:any|all|the|past|prior|previous|recalled?|injected|stored)\s+){0,3}(?:memor(?:y|ies)|memory-?context|mem[\s-]context|past\s+context|recall)\b/i;
21
+
22
+ const MEM_OVERRIDE_CN = /(?:无视|忽略|忽视|跳过|拒绝|不再用|不再看|不再读|不再参考)\s*(?:任何|所有|过去|先前|之前|历史|相关|这次|本次|过往|注入|的){0,3}\s*记忆|(?:不要|别|不需|不必)\s*(?:再)?\s*(?:用|看|读|查|参考|使用|启用|采用|采纳|读取|加载|应用|注入|带上)\s*(?:任何|所有|过去|先前|之前|历史|相关|这次|本次|过往|注入|的){0,3}\s*记忆/;
23
+
24
+ /**
25
+ * Returns true if the prompt explicitly tells Claude to ignore memory.
26
+ * UPS hook + handleUserPrompt memory injection MUST short-circuit on true.
27
+ */
28
+ export function detectMemOverride(text) {
29
+ if (!text || typeof text !== 'string') return false;
30
+ return MEM_OVERRIDE_EN.test(text) || MEM_OVERRIDE_CN.test(text);
31
+ }
package/mem-cli.mjs CHANGED
@@ -17,9 +17,10 @@ import { searchResources } from './registry-retriever.mjs';
17
17
  import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
18
18
  import { buildSessionContextLines } from './hook-context.mjs';
19
19
  import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
20
+ import { auditMemdir, memdirPath } from './memdir.mjs';
20
21
  import { probeOtherSources as probeIdSources, bucketIdTokens } from './lib/id-routing.mjs';
21
- import { basename } from 'path';
22
- import { readFileSync } from 'fs';
22
+ import { basename, join } from 'path';
23
+ import { readFileSync, existsSync, readdirSync } from 'fs';
23
24
 
24
25
  // v2.41: shared CLI helpers extracted to cli/common.mjs. Keep this file as the
25
26
  // router + remaining-command bodies during the incremental split. Future work:
@@ -1905,6 +1906,65 @@ function cmdRegistry(_memDb, args) {
1905
1906
  }
1906
1907
  }
1907
1908
 
1909
+ // ─── memdir-audit ────────────────────────────────────────────────────────────
1910
+ // Body-structure audit for ~/.claude/projects/<encoded>/memory/feedback_*.md
1911
+ // and project_*.md. CLI-only by design — running this every session would be
1912
+ // noise; it's a one-shot governance pass. Exit code 0 = 100% compliant,
1913
+ // 1 = at least one file is non-compliant (so it can gate CI if a project
1914
+ // wants to enforce structure).
1915
+
1916
+ function _formatAuditResult(memdir, result) {
1917
+ const lines = [`[mem] memdir audit: ${memdir}`];
1918
+ const fmt = (label, list) =>
1919
+ list.length ? `${label} (${list.length}):\n - ${list.join('\n - ')}` : `${label} (0)`;
1920
+ lines.push(fmt('Compliant', result.compliant));
1921
+ lines.push(fmt('Missing **Why:**', result.missingWhy));
1922
+ lines.push(fmt('Missing **How to apply:**', result.missingHowToApply));
1923
+ lines.push(fmt('Missing both', result.missingBoth));
1924
+ lines.push(`Total: ${result.total} file(s) (${result.compliant.length} compliant)`);
1925
+ return lines.join('\n');
1926
+ }
1927
+
1928
+ function _resolveMemdirsForAudit(flags) {
1929
+ if (typeof flags.memdir === 'string' && flags.memdir.length > 0) {
1930
+ return [flags.memdir];
1931
+ }
1932
+ if (flags.all === true || flags.all === 'true') {
1933
+ const projectsRoot = join(homedir(), '.claude', 'projects');
1934
+ if (!existsSync(projectsRoot)) return [];
1935
+ let entries;
1936
+ try { entries = readdirSync(projectsRoot); } catch { return []; }
1937
+ return entries
1938
+ .map(name => join(projectsRoot, name, 'memory'))
1939
+ .filter(p => existsSync(p))
1940
+ .sort();
1941
+ }
1942
+ return [memdirPath(process.cwd())];
1943
+ }
1944
+
1945
+ function cmdMemdirAudit(args) {
1946
+ const { flags } = parseArgs(args);
1947
+ const memdirs = _resolveMemdirsForAudit(flags);
1948
+ if (memdirs.length === 0) {
1949
+ out('[mem] No memdirs to audit (use --memdir <path> or run inside a Claude Code project).');
1950
+ return;
1951
+ }
1952
+ let nonCompliant = 0;
1953
+ let totalScanned = 0;
1954
+ for (const md of memdirs) {
1955
+ const result = auditMemdir(md);
1956
+ out(_formatAuditResult(md, result));
1957
+ totalScanned += result.total;
1958
+ nonCompliant +=
1959
+ result.missingWhy.length + result.missingHowToApply.length + result.missingBoth.length;
1960
+ if (memdirs.length > 1) out('');
1961
+ }
1962
+ if (memdirs.length > 1) {
1963
+ out(`[mem] Scanned ${memdirs.length} memdir(s), ${totalScanned} memory file(s), ${nonCompliant} non-compliant.`);
1964
+ }
1965
+ if (nonCompliant > 0) process.exitCode = 1;
1966
+ }
1967
+
1908
1968
  // ─── Help ────────────────────────────────────────────────────────────────────
1909
1969
 
1910
1970
  function cmdHelp() {
@@ -2046,6 +2106,12 @@ Commands:
2046
2106
  unadopt Precise removal of the sentinel block + plugin_claude_mem_lite.md.
2047
2107
  --all Unadopt every project
2048
2108
 
2109
+ memdir-audit Audit memdir feedback_*.md / project_*.md for the
2110
+ body-structure contract (**Why:** + **How to apply:**).
2111
+ Exit 0 if every file is compliant, 1 otherwise.
2112
+ --memdir <path> Audit an explicit memdir path (escape hatch)
2113
+ --all Audit every project under ~/.claude/projects/*/memory/
2114
+
2049
2115
  DB: ${DB_PATH}`);
2050
2116
  }
2051
2117
 
@@ -2240,6 +2306,7 @@ export async function run(argv) {
2240
2306
  // no DB needed. Route them before ensureDb() so an unbootable DB doesn't block.
2241
2307
  if (cmd === 'adopt') { cmdAdopt(cmdArgs); return; }
2242
2308
  if (cmd === 'unadopt') { cmdUnadopt(cmdArgs); return; }
2309
+ if (cmd === 'memdir-audit') { cmdMemdirAudit(cmdArgs); return; }
2243
2310
 
2244
2311
  let db;
2245
2312
  try {
package/memdir.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // See docs/plans/2026-04-16-invited-memory-pattern.md for rationale.
11
11
 
12
- import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, mkdirSync } from 'fs';
12
+ import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, mkdirSync, readdirSync } from 'fs';
13
13
  import { join } from 'path';
14
14
  import { homedir } from 'os';
15
15
  import { createHash } from 'crypto';
@@ -268,3 +268,67 @@ export function removePluginDoc(memdir, slug) {
268
268
  if (!existsSync(path)) return;
269
269
  try { unlinkSync(path); } catch { /* best-effort */ }
270
270
  }
271
+
272
+ // ─── P2: body-structure audit ────────────────────────────────────────────────
273
+ // CC's CLAUDE.md memory contract requires feedback_*.md and project_*.md to
274
+ // carry **Why:** + **How to apply:** lines. user_*.md and reference_*.md
275
+ // have no body-structure requirement (per memoryTypes.ts <body_structure>
276
+ // blocks). MEMORY.md (the index) is excluded too — it lists pointers, not
277
+ // memory content. This is intentionally a CLI-only tool (not a hook): it
278
+ // is a one-shot governance pass, running it on every session would just be
279
+ // noise.
280
+
281
+ const AUDIT_FILE_RE = /^(feedback|project)_[A-Za-z0-9_-]+\.md$/;
282
+ const WHY_RE = /^\s*\*\*Why:\*\*/m;
283
+ const HOW_RE = /^\s*\*\*How to apply:\*\*/m;
284
+
285
+ /**
286
+ * Strip the leading YAML frontmatter block (between `---` fences) so audit
287
+ * checks run only against body content. Returns input unchanged if no
288
+ * frontmatter is present.
289
+ */
290
+ function stripFrontmatter(content) {
291
+ if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return content;
292
+ const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
293
+ return m ? content.slice(m[0].length) : content;
294
+ }
295
+
296
+ /**
297
+ * Scan a memdir for feedback_* and project_* files and bucket them by
298
+ * body-structure compliance. Pure function — IO is read-only and bounded
299
+ * to the directory listing + per-file Reads.
300
+ *
301
+ * @param {string} memdir Absolute path to memdir
302
+ * @returns {{
303
+ * compliant: string[],
304
+ * missingWhy: string[],
305
+ * missingHowToApply: string[],
306
+ * missingBoth: string[],
307
+ * total: number,
308
+ * }}
309
+ */
310
+ export function auditMemdir(memdir) {
311
+ const result = { compliant: [], missingWhy: [], missingHowToApply: [], missingBoth: [], total: 0 };
312
+ if (!memdir || !existsSync(memdir)) return result;
313
+
314
+ let entries;
315
+ try { entries = readdirSync(memdir); } catch { return result; }
316
+
317
+ const targets = entries.filter(n => AUDIT_FILE_RE.test(n)).sort();
318
+ for (const name of targets) {
319
+ let body = '';
320
+ try {
321
+ const raw = readFileSync(join(memdir, name), 'utf8');
322
+ body = stripFrontmatter(raw);
323
+ } catch { /* unreadable — count as missingBoth */ }
324
+
325
+ const hasWhy = WHY_RE.test(body);
326
+ const hasHow = HOW_RE.test(body);
327
+ if (hasWhy && hasHow) result.compliant.push(name);
328
+ else if (!hasWhy && !hasHow) result.missingBoth.push(name);
329
+ else if (!hasWhy) result.missingWhy.push(name);
330
+ else result.missingHowToApply.push(name);
331
+ }
332
+ result.total = targets.length;
333
+ return result;
334
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.59.0",
3
+ "version": "2.60.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -58,6 +58,7 @@
58
58
  "lib/id-routing.mjs",
59
59
  "lib/err-sampler.mjs",
60
60
  "lib/metrics.mjs",
61
+ "lib/mem-override.mjs",
61
62
  "cli/common.mjs",
62
63
  "cli/fts-check.mjs",
63
64
  "cli/doctor.mjs",
@@ -101,6 +101,12 @@ export function detectIntent(text) {
101
101
  return first;
102
102
  }
103
103
 
104
+ // detectMemOverride lives in lib/mem-override.mjs (importable from hook.mjs
105
+ // without dragging the scripts/ tree into SOURCE_FILES). Re-exported here so
106
+ // scripts/user-prompt-search.js and existing tests can keep importing it
107
+ // from the same module as the rest of the prompt-side helpers.
108
+ export { detectMemOverride } from '../lib/mem-override.mjs';
109
+
104
110
  // ─── Error Signature Extraction ─────────────────────────────────────────────
105
111
 
106
112
  /**
@@ -9,7 +9,7 @@ import { cjkPrecisionOk } from '../nlp.mjs';
9
9
  import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import Database from 'better-sqlite3';
12
- import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, DEDUP_STALE_MS, matchRegistrySkillName } from './prompt-search-utils.mjs';
12
+ import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, DEDUP_STALE_MS, matchRegistrySkillName, detectMemOverride } from './prompt-search-utils.mjs';
13
13
 
14
14
  // ─── Constants ──────────────────────────────────────────────────────────────
15
15
 
@@ -491,6 +491,12 @@ async function main() {
491
491
  // Skip short/confirmation/slash-command/simple-op prompts
492
492
  if (shouldSkip(promptText)) return;
493
493
 
494
+ // P0: User-explicit "ignore memory" override (mirrors CC built-in
495
+ // memoryTypes.ts:215). When the prompt directly tells Claude to skip
496
+ // memory recall, we short-circuit before FTS — no FTS budget burn,
497
+ // no .claude-mem-injected-* state churn, no surface emission.
498
+ if (detectMemOverride(promptText)) return;
499
+
494
500
  // T3 (v2.31): additional raw-length gate on top of shouldSkip's CJK-weighted
495
501
  // effective-length check. Suppresses medium-short Latin prompts ("run tests",
496
502
  // "fix bug now") that carry too few content tokens for a meaningful FTS lookup.
package/source-files.mjs CHANGED
@@ -53,6 +53,11 @@ export const SOURCE_FILES = [
53
53
  'memdir.mjs',
54
54
  'adopt-content.mjs',
55
55
  'adopt-cli.mjs',
56
+ // P0 (v2.59.x): user-explicit "ignore memory" override detector. Lives
57
+ // under lib/ (not scripts/) so hook.mjs can statically import it without
58
+ // colliding with the scripts/ directory rename in installExtractedRelease
59
+ // — see the SWITCHABLE_PATHS loop in hook-update.mjs.
60
+ 'lib/mem-override.mjs',
56
61
  ];
57
62
 
58
63
  /**