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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.24.2",
13
+ "version": "2.25.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.24.2",
3
+ "version": "2.25.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-episode.mjs CHANGED
@@ -133,7 +133,6 @@ export function createEpisode(sessionId, project) {
133
133
  files: [],
134
134
  entries: [],
135
135
  filesRead: [],
136
- fileHistoryShown: [],
137
136
  };
138
137
  }
139
138
 
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, recallForFile } from './hook-memory.mjs';
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
- // Proactive file history: show past observations for files being edited
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 COALESCE(importance, 1) >= 2
657
- ORDER BY created_at_epoch DESC LIMIT 5
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
- summaryLines.push('### Key Context');
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 lesson = o.lesson_learned ? ` — ${truncate(o.lesson_learned, 60)}` : '';
667
- summaryLines.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
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.24.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
+ }