claude-mem-lite 2.80.0 → 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.0",
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.0",
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: {
package/install.mjs CHANGED
@@ -1517,10 +1517,20 @@ function hasMemHooksConfigured(settings) {
1517
1517
  * node "/home/sds/.claude-mem-lite/hook.mjs" session-start
1518
1518
  * bash "/home/sds/.claude-mem-lite/scripts/post-tool-use.sh"
1519
1519
  * node "/home/sds/.claude-mem-lite/scripts/pre-tool-recall.js"
1520
- * We pick the first quoted absolute path; if there is no quoted token we fall
1521
- * back to the first whitespace-delimited absolute-looking token after the
1522
- * interpreter. ${CLAUDE_PLUGIN_ROOT}-templated commands are ignored those
1523
- * are plugin-owned hooks resolved by Claude Code at runtime, not by us.
1520
+ *
1521
+ * Scan order (v2.80+): walk EVERY quoted token via matchAll, prefer ones that
1522
+ * look like a hook path (absolute + ends in a known hook-runtime extension).
1523
+ * If no quoted token qualifies, fall back to the first path-shaped token from
1524
+ * a whitespace-split of the command. If both miss, skip the entry entirely —
1525
+ * deliberate bias toward **under-reporting over false-flagging**: a wrapper
1526
+ * like `bash -c "inline" "/real/path.sh"` should report the real path, not
1527
+ * the inline string. ${CLAUDE_PLUGIN_ROOT}-templated commands are ignored —
1528
+ * those are plugin-owned hooks resolved by Claude Code at runtime, not by us.
1529
+ *
1530
+ * Extension list (HOOK_PATH_EXTS) is hardcoded for the runtimes this plugin
1531
+ * actually registers (node/bash). Extend if Claude Code ever supports new
1532
+ * hook runtimes (e.g. python/.py). Currently safe because isMemHook() filters
1533
+ * to claude-mem-lite-owned hooks only — foreign runtimes can't reach here.
1524
1534
  */
1525
1535
  const HOOK_PATH_EXTS = ['.mjs', '.js', '.cjs', '.sh'];
1526
1536
 
@@ -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.0",
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/scripts/setup.sh CHANGED
@@ -85,7 +85,7 @@ mark_deps_broken() {
85
85
  # Embed reason + repair command so hook.mjs renders a complete error without
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
- # (bash `printf '"..%s.."'` cannot escape arbitrary strings safely; v2.79 fix).
88
+ # (bash `printf '"..%s.."'` cannot escape arbitrary strings safely; v2.79.1 fix).
89
89
  MARK_REASON="$reason" MARK_ROOT="$ROOT" MARK_FLAG="$DEPS_FLAG" node -e '
90
90
  const fs = require("fs");
91
91
  const reason = process.env.MARK_REASON || "unknown";
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',