claude-mem-lite 2.80.1 → 2.81.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.80.1",
13
+ "version": "2.81.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.80.1",
3
+ "version": "2.81.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/hook.mjs CHANGED
@@ -61,6 +61,7 @@ import { checkForUpdate } from './hook-update.mjs';
61
61
  import { handleLLMOptimize } from './hook-optimize.mjs';
62
62
  import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
63
63
  import { emitV270UpgradeBanner } from './lib/upgrade-banner.mjs';
64
+ import { loadCiteBackForEpisode } from './lib/cite-back-hint.mjs';
64
65
  // plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
65
66
  // from an older hook-update.mjs SOURCE_FILES (which did not list this module) would
66
67
  // crash on static import. Degrade gracefully to no-op when the module is absent.
@@ -196,6 +197,12 @@ function flushEpisode(episode, hookEventName = 'PostToolUse') {
196
197
  const filesHint = uniqueFiles.length > 0 ? ` (${uniqueFiles.join(', ')})` : '';
197
198
  lines.push(`[mem] 💡 error→fix pattern${filesHint} — consider: mem_save(type="bugfix", lesson_learned="<root cause + fix>")`);
198
199
  }
200
+ // v2.81: cite-back hint — fires when this episode edits a file that
201
+ // PreToolUse:Read/Edit nudged earlier in the same session. Precision
202
+ // signal (we know the file was warned about); orthogonal to the
203
+ // error→fix pattern above and may co-fire.
204
+ const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
205
+ if (citeBack) lines.push(citeBack);
199
206
  process.stdout.write(JSON.stringify({
200
207
  suppressOutput: true,
201
208
  hookSpecificOutput: {
@@ -0,0 +1,70 @@
1
+ // claude-mem-lite: PostToolUse cite-back hint builder.
2
+ //
3
+ // Fires when a flushed episode edits a file that PreToolUse:Read/Edit had
4
+ // nudged earlier in the same session — the canonical "you fixed something we
5
+ // warned about, save the lesson?" moment.
6
+ //
7
+ // Pure function: takes an episode + the session-scoped pre-recall cooldown
8
+ // object and returns a hint string (or null). Cooldown I/O lives elsewhere so
9
+ // this stays unit-testable without disk fixtures.
10
+ //
11
+ // Cooldown schema (post-v2.81): { "<path>": { ts: <number>, lessonIds: [#NN, ...] } }
12
+ // Legacy schema (pre-v2.81): { "<path>": <number> } — tolerated, never emits.
13
+
14
+ import { basename, join } from 'path';
15
+ import { readFileSync } from 'fs';
16
+ import { EDIT_TOOLS } from '../utils.mjs';
17
+
18
+ const MAX_FILES = 2;
19
+
20
+ export function buildCiteBackHint(episode, cooldown) {
21
+ if (!episode || !cooldown) return null;
22
+ const entries = episode.entries;
23
+ if (!Array.isArray(entries) || entries.length === 0) return null;
24
+
25
+ const seen = new Set();
26
+ const matches = [];
27
+ for (const e of entries) {
28
+ if (!EDIT_TOOLS.has(e.tool)) continue;
29
+ for (const file of e.files || []) {
30
+ if (seen.has(file)) continue;
31
+ const entry = cooldown[file];
32
+ if (!entry || typeof entry !== 'object') continue;
33
+ const ids = Array.isArray(entry.lessonIds) ? entry.lessonIds : null;
34
+ if (!ids || ids.length === 0) continue;
35
+ seen.add(file);
36
+ matches.push({ file, ids });
37
+ if (matches.length >= MAX_FILES) break;
38
+ }
39
+ if (matches.length >= MAX_FILES) break;
40
+ }
41
+
42
+ if (matches.length === 0) return null;
43
+
44
+ const lines = ['[mem] 💡 Cite-back: you edited file(s) that received PreToolUse lessons this session.'];
45
+ for (const m of matches) {
46
+ const fname = basename(m.file);
47
+ const idList = m.ids.map(id => `#${id}`).join(', ');
48
+ lines.push(` • ${fname} ← ${idList} — if you fixed it: /lesson --file ${fname} "<root cause + fix>"`);
49
+ }
50
+ return lines.join('\n');
51
+ }
52
+
53
+ // Path scheme MUST mirror scripts/pre-tool-recall.js cooldownPathFor() — drift
54
+ // silently zeros cite-back across the release. Pinned by tests/cite-back-hint.test.mjs
55
+ // 'sanitizes the sessionId the same way pre-tool-recall.js does'.
56
+ function cooldownPathFor(sessionId, runtimeDir) {
57
+ const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
58
+ return join(runtimeDir, `pre-recall-cooldown-${safe}.json`);
59
+ }
60
+
61
+ export function loadCiteBackForEpisode(episode, runtimeDir) {
62
+ if (!episode || !episode.sessionId || !runtimeDir) return null;
63
+ let cooldown;
64
+ try {
65
+ cooldown = JSON.parse(readFileSync(cooldownPathFor(episode.sessionId, runtimeDir), 'utf8'));
66
+ } catch {
67
+ return null;
68
+ }
69
+ return buildCiteBackHint(episode, cooldown);
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.80.1",
3
+ "version": "2.81.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -57,6 +57,7 @@
57
57
  "lib/low-signal-patterns.mjs",
58
58
  "lib/private-strip.mjs",
59
59
  "lib/citation-tracker.mjs",
60
+ "lib/cite-back-hint.mjs",
60
61
  "lib/summary-extractor.mjs",
61
62
  "lib/id-routing.mjs",
62
63
  "lib/err-sampler.mjs",
@@ -49,6 +49,15 @@ function readCooldown(cooldownPath) {
49
49
  try { return JSON.parse(readFileSync(cooldownPath, 'utf8')); } catch { return {}; }
50
50
  }
51
51
 
52
+ // v2.81: cooldown entries are {ts, lessonIds} objects so the PostToolUse
53
+ // cite-back hint can name the lessons that were nudged. Legacy entries
54
+ // (pre-v2.81) are bare numbers — entryTimestamp() reads both shapes.
55
+ function entryTimestamp(v) {
56
+ if (typeof v === 'number') return v;
57
+ if (v && typeof v === 'object' && typeof v.ts === 'number') return v.ts;
58
+ return 0;
59
+ }
60
+
52
61
  function writeCooldown(cooldownPath, data, isSessionScoped) {
53
62
  try {
54
63
  mkdirSync(RUNTIME_DIR, { recursive: true });
@@ -59,7 +68,8 @@ function writeCooldown(cooldownPath, data, isSessionScoped) {
59
68
  const cleaned = isSessionScoped ? data : {};
60
69
  if (!isSessionScoped) {
61
70
  for (const [k, v] of Object.entries(data)) {
62
- if (now - v < STALE_MS) cleaned[k] = v;
71
+ const ts = entryTimestamp(v);
72
+ if (ts && now - ts < STALE_MS) cleaned[k] = v;
63
73
  }
64
74
  }
65
75
  writeFileSync(cooldownPath, JSON.stringify(cleaned));
@@ -124,7 +134,8 @@ try {
124
134
  if (isSessionScoped) {
125
135
  if (cooldown[filePath]) process.exit(0); // already recalled this file in-session
126
136
  } else {
127
- if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) process.exit(0);
137
+ const ts = entryTimestamp(cooldown[filePath]);
138
+ if (ts && (now - ts) < COOLDOWN_MS) process.exit(0);
128
139
  }
129
140
 
130
141
  // Open DB readonly
@@ -275,7 +286,10 @@ try {
275
286
  // Cooldown applies on ALL branches (including silent-Read) so subsequent
276
287
  // calls on the same file in the same session don't re-query — preserving
277
288
  // the per-filePath invariant that underpins Read→Edit dedup.
278
- cooldown[filePath] = now;
289
+ // v2.81: record the emitted lesson IDs so flushEpisode (hook.mjs) can
290
+ // build the PostToolUse cite-back hint when the user actually edits the
291
+ // file. Empty array on no-lesson branches keeps the schema uniform.
292
+ cooldown[filePath] = { ts: now, lessonIds: allRows.map(r => r.id) };
279
293
  writeCooldown(cooldownPath, cooldown, isSessionScoped);
280
294
  } catch (e) {
281
295
  // Silent failure — never block editing, but record for self-observation.
package/source-files.mjs CHANGED
@@ -40,6 +40,7 @@ export const SOURCE_FILES = [
40
40
  'lib/low-signal-patterns.mjs',
41
41
  'lib/private-strip.mjs',
42
42
  'lib/citation-tracker.mjs',
43
+ 'lib/cite-back-hint.mjs',
43
44
  'lib/summary-extractor.mjs',
44
45
  'lib/id-routing.mjs',
45
46
  'lib/err-sampler.mjs',