claude-mem-lite 2.24.2 → 2.25.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-episode.mjs +0 -1
- package/hook.mjs +38 -32
- package/hooks/hooks.json +12 -0
- package/install.mjs +13 -8
- package/package.json +2 -1
- package/scripts/pre-tool-recall.js +126 -0
package/hook-episode.mjs
CHANGED
package/hook.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
spawnBackground,
|
|
29
29
|
} from './hook-shared.mjs';
|
|
30
30
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
31
|
-
import { searchRelevantMemories
|
|
31
|
+
import { searchRelevantMemories } from './hook-memory.mjs';
|
|
32
32
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
33
33
|
import { checkForUpdate } from './hook-update.mjs';
|
|
34
34
|
import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
|
|
@@ -225,28 +225,7 @@ async function handlePostToolUse() {
|
|
|
225
225
|
episode.lastAt = Date.now();
|
|
226
226
|
addFileToEpisode(episode, files);
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
// Uses recallForFile for importance>=2 with lesson context
|
|
230
|
-
if (EDIT_TOOLS.has(tool_name) && files.length > 0) {
|
|
231
|
-
const d = getDb();
|
|
232
|
-
if (d) {
|
|
233
|
-
for (const f of files) {
|
|
234
|
-
if (episode.fileHistoryShown?.includes(f)) continue;
|
|
235
|
-
try {
|
|
236
|
-
const recalled = recallForFile(d, f, project);
|
|
237
|
-
if (recalled.length > 0) {
|
|
238
|
-
const hints = recalled.map(r => {
|
|
239
|
-
const lesson = r.lesson_learned ? ` | ${r.lesson_learned}` : '';
|
|
240
|
-
return ` #${r.id} [${r.type}] ${truncate(r.title, 60)}${lesson}`;
|
|
241
|
-
}).join('\n');
|
|
242
|
-
process.stdout.write(`[claude-mem-lite] History for ${basename(f)}:\n${hints}\n`);
|
|
243
|
-
}
|
|
244
|
-
} catch (e) { debugCatch(e, 'fileHistory'); }
|
|
245
|
-
if (!episode.fileHistoryShown) episode.fileHistoryShown = [];
|
|
246
|
-
episode.fileHistoryShown.push(f);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
228
|
+
// File history injection moved to PreToolUse hook (scripts/pre-tool-recall.js)
|
|
250
229
|
|
|
251
230
|
writeEpisode(episode);
|
|
252
231
|
|
|
@@ -650,23 +629,50 @@ async function handleSessionStart() {
|
|
|
650
629
|
const summaryLines = buildSummaryLines(latestSummary);
|
|
651
630
|
|
|
652
631
|
// Key context: top high-importance observations for CLAUDE.md persistence
|
|
632
|
+
// Split into "File Lessons" (actionable, has lesson + file) and "Key Context" (informational)
|
|
653
633
|
const keyObs = db.prepare(`
|
|
654
|
-
SELECT id, type, title, lesson_learned FROM observations
|
|
655
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0
|
|
656
|
-
AND
|
|
657
|
-
|
|
634
|
+
SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
|
|
635
|
+
WHERE o.project = ? AND COALESCE(o.compressed_into, 0) = 0
|
|
636
|
+
AND o.superseded_at IS NULL
|
|
637
|
+
AND COALESCE(o.importance, 1) >= 2
|
|
638
|
+
ORDER BY o.created_at_epoch DESC LIMIT 10
|
|
658
639
|
`).all(project);
|
|
640
|
+
|
|
659
641
|
if (keyObs.length > 0) {
|
|
660
|
-
|
|
642
|
+
const fileLessons = [];
|
|
643
|
+
const keyContext = [];
|
|
644
|
+
|
|
661
645
|
for (const o of keyObs) {
|
|
662
|
-
// Strip raw JSON output from degraded Bash-style titles
|
|
663
646
|
const clean = (o.title || '(untitled)')
|
|
664
647
|
.replace(/ → (?:ERROR: )?\{".*$/, '')
|
|
665
648
|
.replace(/ → (?:ERROR: )?\{[^}]*\.{3}$/, '');
|
|
666
|
-
const
|
|
667
|
-
|
|
649
|
+
const hasLesson = o.lesson_learned && o.lesson_learned.trim();
|
|
650
|
+
const hasFiles = o.files_modified && o.files_modified !== '[]';
|
|
651
|
+
|
|
652
|
+
if (hasLesson && hasFiles) {
|
|
653
|
+
try {
|
|
654
|
+
const files = JSON.parse(o.files_modified);
|
|
655
|
+
const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
|
|
656
|
+
if (fname) {
|
|
657
|
+
fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
} catch {}
|
|
661
|
+
}
|
|
662
|
+
const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
|
|
663
|
+
keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (fileLessons.length > 0) {
|
|
667
|
+
summaryLines.push('### File Lessons');
|
|
668
|
+
summaryLines.push(...fileLessons.slice(0, 5));
|
|
669
|
+
summaryLines.push('');
|
|
670
|
+
}
|
|
671
|
+
if (keyContext.length > 0) {
|
|
672
|
+
summaryLines.push('### Key Context');
|
|
673
|
+
summaryLines.push(...keyContext.slice(0, 5));
|
|
674
|
+
summaryLines.push('');
|
|
668
675
|
}
|
|
669
|
-
summaryLines.push('');
|
|
670
676
|
} else if (!latestSummary) {
|
|
671
677
|
// Fallback: no summary AND no key observations — show recent activity
|
|
672
678
|
const recentObs = (observations.length >= 3 ? observations : fallbackObs).slice(0, 3);
|
package/hooks/hooks.json
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
|
+
"PreToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Edit|Write|NotebookEdit",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-recall.js\"",
|
|
28
|
+
"timeout": 3
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
21
33
|
"PostToolUse": [
|
|
22
34
|
{
|
|
23
35
|
"matcher": "*",
|
package/install.mjs
CHANGED
|
@@ -452,20 +452,25 @@ async function install() {
|
|
|
452
452
|
]
|
|
453
453
|
};
|
|
454
454
|
|
|
455
|
+
const memPreToolUse = {
|
|
456
|
+
matcher: 'Edit|Write|NotebookEdit',
|
|
457
|
+
hooks: [
|
|
458
|
+
{
|
|
459
|
+
type: 'command',
|
|
460
|
+
command: `node "${join(SCRIPTS_PATH, 'pre-tool-recall.js')}"`,
|
|
461
|
+
timeout: 3
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
|
|
455
466
|
// Filter out existing mem hooks, then append fresh ones
|
|
456
|
-
for (const [event, config] of [['PostToolUse', memPostToolUse], ['SessionStart', memSessionStart], ['Stop', memStop], ['UserPromptSubmit', memUserPrompt]]) {
|
|
467
|
+
for (const [event, config] of [['PreToolUse', memPreToolUse], ['PostToolUse', memPostToolUse], ['SessionStart', memSessionStart], ['Stop', memStop], ['UserPromptSubmit', memUserPrompt]]) {
|
|
457
468
|
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event].filter(cfg => !isMemHook(cfg)) : [];
|
|
458
469
|
settings.hooks[event] = [...existing, config];
|
|
459
470
|
}
|
|
460
471
|
|
|
461
|
-
// Clean up stale PreToolUse hook from previous versions
|
|
462
|
-
if (Array.isArray(settings.hooks.PreToolUse)) {
|
|
463
|
-
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(cfg => !isMemHook(cfg));
|
|
464
|
-
if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
472
|
writeSettings(settings);
|
|
468
|
-
ok('Hooks configured (PostToolUse, SessionStart, Stop, UserPromptSubmit)');
|
|
473
|
+
ok('Hooks configured (PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit)');
|
|
469
474
|
|
|
470
475
|
// 5. Migrate from old ~/.claude-mem/ if needed
|
|
471
476
|
if (existsSync(join(OLD_DATA_DIR, 'claude-mem.db')) && !existsSync(DB_PATH) && !existsSync(join(DATA_DIR, 'claude-mem.db'))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"scripts/setup.sh",
|
|
73
73
|
"scripts/post-tool-use.sh",
|
|
74
74
|
"scripts/user-prompt-search.js",
|
|
75
|
+
"scripts/pre-tool-recall.js",
|
|
75
76
|
"scripts/prompt-search-utils.mjs",
|
|
76
77
|
".mcp.json",
|
|
77
78
|
".claude-plugin/plugin.json",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// claude-mem-lite: PreToolUse file recall — injects lessons before Edit/Write
|
|
3
|
+
// Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os
|
|
4
|
+
// Safety: readonly DB, exit 0 always, 3s timeout
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { basename, join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const DB_PATH = join(homedir(), '.claude-mem-lite', 'claude-mem-lite.db');
|
|
11
|
+
const RUNTIME_DIR = join(homedir(), '.claude-mem-lite', 'runtime');
|
|
12
|
+
const COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
13
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
const STALE_MS = 10 * 60 * 1000; // 10 minutes cleanup threshold
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function inferProject() {
|
|
19
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
20
|
+
const base = basename(dir);
|
|
21
|
+
const parent = basename(join(dir, '..'));
|
|
22
|
+
let project = (parent && parent !== '.' && parent !== '/')
|
|
23
|
+
? `${parent}--${base}` : base;
|
|
24
|
+
project = project.replace(/[^a-zA-Z0-9_.-]/g, '-') || 'unknown';
|
|
25
|
+
return project;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readCooldown() {
|
|
29
|
+
try { return JSON.parse(readFileSync(COOLDOWN_PATH, 'utf8')); } catch { return {}; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeCooldown(data) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
35
|
+
// Clean stale entries
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const cleaned = {};
|
|
38
|
+
for (const [k, v] of Object.entries(data)) {
|
|
39
|
+
if (now - v < STALE_MS) cleaned[k] = v;
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(COOLDOWN_PATH, JSON.stringify(cleaned));
|
|
42
|
+
} catch { /* silent */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Skip if recursive hook
|
|
49
|
+
if (process.env.CLAUDE_MEM_HOOK_RUNNING) process.exit(0);
|
|
50
|
+
|
|
51
|
+
// Skip if DB doesn't exist
|
|
52
|
+
if (!existsSync(DB_PATH)) process.exit(0);
|
|
53
|
+
|
|
54
|
+
// Read stdin
|
|
55
|
+
let input = '';
|
|
56
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
57
|
+
|
|
58
|
+
// Parse event
|
|
59
|
+
let filePath;
|
|
60
|
+
try {
|
|
61
|
+
const event = JSON.parse(input);
|
|
62
|
+
filePath = event.tool_input?.file_path;
|
|
63
|
+
} catch { process.exit(0); }
|
|
64
|
+
|
|
65
|
+
if (!filePath) process.exit(0);
|
|
66
|
+
|
|
67
|
+
// Cooldown check (full path as key)
|
|
68
|
+
const cooldown = readCooldown();
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Open DB readonly
|
|
75
|
+
const Database = (await import('better-sqlite3')).default;
|
|
76
|
+
let db;
|
|
77
|
+
try {
|
|
78
|
+
db = new Database(DB_PATH, { readonly: true });
|
|
79
|
+
db.pragma('busy_timeout = 1000');
|
|
80
|
+
} catch { process.exit(0); }
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const project = inferProject();
|
|
84
|
+
const fname = basename(filePath);
|
|
85
|
+
// Escape LIKE wildcards
|
|
86
|
+
const escaped = fname.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
87
|
+
const likePattern = `%${escaped}`;
|
|
88
|
+
// 60-day lookback to avoid surfacing ancient observations
|
|
89
|
+
const cutoff = Date.now() - 60 * 86400000;
|
|
90
|
+
|
|
91
|
+
const rows = db.prepare(`
|
|
92
|
+
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned
|
|
93
|
+
FROM observations o
|
|
94
|
+
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
95
|
+
WHERE o.project = ?
|
|
96
|
+
AND o.importance >= 2
|
|
97
|
+
AND o.lesson_learned IS NOT NULL
|
|
98
|
+
AND o.lesson_learned != ''
|
|
99
|
+
AND COALESCE(o.compressed_into, 0) = 0
|
|
100
|
+
AND o.superseded_at IS NULL
|
|
101
|
+
AND o.created_at_epoch > ?
|
|
102
|
+
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
103
|
+
ORDER BY o.created_at_epoch DESC
|
|
104
|
+
LIMIT 2
|
|
105
|
+
`).all(project, cutoff, filePath, likePattern);
|
|
106
|
+
|
|
107
|
+
if (rows.length > 0) {
|
|
108
|
+
console.log(`[mem] Lessons for ${fname}:`);
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
const lesson = r.lesson_learned.length > 120
|
|
111
|
+
? r.lesson_learned.slice(0, 117) + '...'
|
|
112
|
+
: r.lesson_learned;
|
|
113
|
+
console.log(` #${r.id} [${r.type}] ${lesson}`);
|
|
114
|
+
}
|
|
115
|
+
// Update cooldown
|
|
116
|
+
cooldown[filePath] = now;
|
|
117
|
+
writeCooldown(cooldown);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Silent failure — never block editing
|
|
121
|
+
} finally {
|
|
122
|
+
try { db.close(); } catch {}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Top-level catch — exit 0 no matter what
|
|
126
|
+
}
|