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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook.mjs +7 -0
- package/lib/cite-back-hint.mjs +70 -0
- package/package.json +2 -1
- package/scripts/pre-tool-recall.js +17 -3
- package/source-files.mjs +1 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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