claude-mem-lite 2.33.0 → 2.33.1

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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +0 -0
  4. package/LICENSE +0 -0
  5. package/README.md +0 -0
  6. package/README.zh-CN.md +0 -0
  7. package/adopt-cli.mjs +0 -0
  8. package/adopt-content.mjs +0 -0
  9. package/bash-utils.mjs +0 -0
  10. package/commands/adopt.md +0 -0
  11. package/commands/bug.md +0 -0
  12. package/commands/lesson.md +0 -0
  13. package/commands/mem.md +0 -0
  14. package/commands/memory.md +0 -0
  15. package/commands/tools.md +0 -0
  16. package/commands/unadopt.md +0 -0
  17. package/commands/update.md +0 -0
  18. package/format-utils.mjs +0 -0
  19. package/haiku-client.mjs +0 -0
  20. package/hash-utils.mjs +0 -0
  21. package/hook-context.mjs +0 -0
  22. package/hook-episode.mjs +0 -0
  23. package/hook-handoff.mjs +0 -0
  24. package/hook-llm.mjs +17 -5
  25. package/hook-memory.mjs +0 -0
  26. package/hook-optimize.mjs +0 -0
  27. package/hook-semaphore.mjs +0 -0
  28. package/hook-shared.mjs +0 -0
  29. package/hook-update.mjs +0 -0
  30. package/hook.mjs +22 -7
  31. package/hooks/hooks.json +0 -0
  32. package/install-metadata.mjs +0 -0
  33. package/install.mjs +0 -0
  34. package/lib/activity.mjs +0 -0
  35. package/lib/doctor-benchmark.mjs +0 -0
  36. package/lib/git-state.mjs +0 -0
  37. package/lib/plan-reader.mjs +0 -0
  38. package/lib/startup-dashboard.mjs +0 -0
  39. package/lib/task-reader.mjs +0 -0
  40. package/mem-cli.mjs +0 -0
  41. package/memdir.mjs +0 -0
  42. package/nlp.mjs +0 -0
  43. package/package.json +1 -1
  44. package/plugin-cache-guard.mjs +0 -0
  45. package/project-utils.mjs +0 -0
  46. package/registry/preinstalled.json +0 -0
  47. package/registry-enricher.mjs +0 -0
  48. package/registry-github.mjs +0 -0
  49. package/registry-importer.mjs +0 -0
  50. package/registry-indexer.mjs +0 -0
  51. package/registry-retriever.mjs +0 -0
  52. package/registry-scanner.mjs +0 -0
  53. package/registry.mjs +0 -0
  54. package/resource-discovery.mjs +0 -0
  55. package/schema.mjs +0 -0
  56. package/scoring-sql.mjs +0 -0
  57. package/scripts/launch.mjs +0 -0
  58. package/scripts/pre-skill-bridge.js +0 -0
  59. package/scripts/pre-tool-recall.js +58 -17
  60. package/scripts/prompt-search-utils.mjs +0 -0
  61. package/scripts/user-prompt-search.js +24 -3
  62. package/secret-scrub.mjs +0 -0
  63. package/server-internals.mjs +0 -0
  64. package/server.mjs +0 -0
  65. package/skill.md +0 -0
  66. package/skip-tools.mjs +0 -0
  67. package/source-files.mjs +0 -0
  68. package/stop-words.mjs +0 -0
  69. package/synonyms.mjs +0 -0
  70. package/tfidf.mjs +0 -0
  71. package/tier.mjs +0 -0
  72. package/tool-schemas.mjs +0 -0
  73. package/utils.mjs +0 -0
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.33.0",
13
+ "version": "2.33.1",
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.33.0",
3
+ "version": "2.33.1",
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/.mcp.json CHANGED
File without changes
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/README.zh-CN.md CHANGED
File without changes
package/adopt-cli.mjs CHANGED
File without changes
package/adopt-content.mjs CHANGED
File without changes
package/bash-utils.mjs CHANGED
File without changes
package/commands/adopt.md CHANGED
File without changes
package/commands/bug.md CHANGED
File without changes
File without changes
package/commands/mem.md CHANGED
File without changes
File without changes
package/commands/tools.md CHANGED
File without changes
File without changes
File without changes
package/format-utils.mjs CHANGED
File without changes
package/haiku-client.mjs CHANGED
File without changes
package/hash-utils.mjs CHANGED
File without changes
package/hook-context.mjs CHANGED
File without changes
package/hook-episode.mjs CHANGED
File without changes
package/hook-handoff.mjs CHANGED
File without changes
package/hook-llm.mjs CHANGED
@@ -559,10 +559,17 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
559
559
  return;
560
560
  }
561
561
 
562
- const lessonLearned = typeof parsed.lesson_learned === 'string'
563
- && parsed.lesson_learned.toLowerCase() !== 'none'
564
- && parsed.lesson_learned.trim().length > 0
565
- ? parsed.lesson_learned.slice(0, 500) : null;
562
+ // v2.33.1: expanded low-signal filter. Historical data showed Haiku
563
+ // returns 'none'/''/'n/a'/'null'/'-'/'todo'/'tbd' ~95% of the time —
564
+ // all noise with no retrieval value. Also reject lessons <12 chars
565
+ // (e.g. "ok", "works", "fixed it") too short to teach a future session.
566
+ // When filtered, downgrade importance to 0 so rule-based fallback in
567
+ // hook.mjs:saveObservation writes the obs but hook queries (which all
568
+ // require importance >= 1) ignore it.
569
+ const rawLesson = typeof parsed.lesson_learned === 'string' ? parsed.lesson_learned.trim() : '';
570
+ const lowSignalLesson = new Set(['none', '', 'n/a', 'null', 'todo', 'tbd', 'na', '-', 'nothing', 'nil']);
571
+ const isLessonLowSignal = lowSignalLesson.has(rawLesson.toLowerCase()) || rawLesson.length < 12;
572
+ const lessonLearned = isLessonLowSignal ? null : rawLesson.slice(0, 500);
566
573
  const searchAliases = Array.isArray(parsed.search_aliases)
567
574
  ? parsed.search_aliases.slice(0, 6).join(' ')
568
575
  : null;
@@ -576,7 +583,12 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
576
583
  facts: Array.isArray(parsed.facts) ? parsed.facts.slice(0, 10) : [],
577
584
  files: episode.files,
578
585
  filesRead: episode.filesRead || [],
579
- importance: Math.max(ruleImportance, clampImportance(parsed.importance)),
586
+ // v2.33.1: when lesson is low-signal, don't trust Haiku's importance
587
+ // inflation for noise-prone types. rule-based floor still applies so
588
+ // error-in-test (→3) / config-change (→2) keep their floor.
589
+ importance: isLessonLowSignal && (parsed.type === 'change' || parsed.type === 'discovery')
590
+ ? Math.min(ruleImportance, 1)
591
+ : Math.max(ruleImportance, clampImportance(parsed.importance)),
580
592
  lessonLearned,
581
593
  searchAliases,
582
594
  };
package/hook-memory.mjs CHANGED
File without changes
package/hook-optimize.mjs CHANGED
File without changes
File without changes
package/hook-shared.mjs CHANGED
File without changes
package/hook-update.mjs CHANGED
File without changes
package/hook.mjs CHANGED
@@ -135,21 +135,36 @@ function flushEpisode(episode) {
135
135
  if (isSignificant) {
136
136
  spawnBackground('llm-episode', flushFile);
137
137
 
138
- // P3: Auto-save hint detect error→fix pattern (error entry followed by Edit/Write)
139
- // and nudge Claude to save the lesson for future recall
138
+ // v2.33.1: structured flush receipt so Claude sees what mem just captured
139
+ // and the legacy error→fix nudge consolidates here. PostToolUse JSON with
140
+ // hookSpecificOutput.additionalContext reliably renders across CC variants;
141
+ // the old plain-text stdout write was invisible on some variants.
140
142
  try {
141
143
  const entries = episode.entries || [];
142
144
  const hasError = entries.some(e => e.isError);
143
145
  const hasEdit = entries.some(e => EDIT_TOOLS.has(e.tool));
146
+ const toolCounts = {};
147
+ for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
148
+ const toolSummary = Object.entries(toolCounts)
149
+ .sort((a, b) => b[1] - a[1])
150
+ .slice(0, 3)
151
+ .map(([t, n]) => `${t}×${n}`)
152
+ .join(', ');
153
+ const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
144
154
  if (hasError && hasEdit && entries.length >= 3) {
145
155
  const editFiles = entries.filter(e => EDIT_TOOLS.has(e.tool)).flatMap(e => e.files || []);
146
156
  const uniqueFiles = [...new Set(editFiles)].slice(0, 3);
147
- const filesHint = uniqueFiles.length > 0 ? ` (files: ${uniqueFiles.join(', ')})` : '';
148
- process.stdout.write(
149
- `[mem] 💡 Error→fix pattern detected${filesHint}. Consider: mem_save(type="bugfix", lesson_learned="root cause & fix")\n`,
150
- );
157
+ const filesHint = uniqueFiles.length > 0 ? ` (${uniqueFiles.join(', ')})` : '';
158
+ lines.push(`[mem] 💡 error→fix pattern${filesHint} — consider: mem_save(type="bugfix", lesson_learned="<root cause + fix>")`);
151
159
  }
152
- } catch { /* never block on hint */ }
160
+ process.stdout.write(JSON.stringify({
161
+ suppressOutput: true,
162
+ hookSpecificOutput: {
163
+ hookEventName: 'PostToolUse',
164
+ additionalContext: lines.join('\n'),
165
+ },
166
+ }));
167
+ } catch { /* never block on receipt */ }
153
168
  } else {
154
169
  try { unlinkSync(flushFile); } catch {}
155
170
  }
package/hooks/hooks.json CHANGED
File without changes
File without changes
package/install.mjs CHANGED
File without changes
package/lib/activity.mjs CHANGED
File without changes
File without changes
package/lib/git-state.mjs CHANGED
File without changes
File without changes
File without changes
File without changes
package/mem-cli.mjs CHANGED
File without changes
package/memdir.mjs CHANGED
File without changes
package/nlp.mjs CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.33.0",
3
+ "version": "2.33.1",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
File without changes
package/project-utils.mjs CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/registry.mjs CHANGED
File without changes
File without changes
package/schema.mjs CHANGED
File without changes
package/scoring-sql.mjs CHANGED
File without changes
File without changes
File without changes
@@ -3,7 +3,7 @@
3
3
  // Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os
4
4
  // Safety: readonly DB, exit 0 always, 3s timeout
5
5
 
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
7
7
  import { basename, join } from 'path';
8
8
  import { homedir } from 'os';
9
9
 
@@ -11,9 +11,20 @@ import { homedir } from 'os';
11
11
  // point the hook at an isolated DB + cooldown dir without touching the user's real state.
12
12
  const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(homedir(), '.claude-mem-lite', 'claude-mem-lite.db');
13
13
  const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(homedir(), '.claude-mem-lite', 'runtime');
14
- const COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
15
- const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
16
- const STALE_MS = 10 * 60 * 1000; // 10 minutes cleanup threshold
14
+ // v2.33.1: cooldown path is session-scoped so same-file-twice within one
15
+ // session never re-injects (was: global file, 5-min window). Cross-session:
16
+ // fresh file, fresh nudges this is intended. No session_id → fall back to
17
+ // legacy global path so env-less test harnesses still behave.
18
+ const LEGACY_COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
19
+ const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes (used only for legacy fallback)
20
+ const STALE_MS = 10 * 60 * 1000; // 10 minutes cleanup threshold for legacy file
21
+ const SESSION_COOLDOWN_STALE_MS = 24 * 60 * 60 * 1000; // 24h — drop session cooldown files older than this
22
+
23
+ function cooldownPathFor(sessionId) {
24
+ if (!sessionId) return LEGACY_COOLDOWN_PATH;
25
+ const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
26
+ return join(RUNTIME_DIR, `pre-recall-cooldown-${safe}.json`);
27
+ }
17
28
 
18
29
  // ─── Helpers ────────────────────────────────────────────────────────────────
19
30
 
@@ -27,20 +38,40 @@ function inferProject() {
27
38
  return project;
28
39
  }
29
40
 
30
- function readCooldown() {
31
- try { return JSON.parse(readFileSync(COOLDOWN_PATH, 'utf8')); } catch { return {}; }
41
+ function readCooldown(cooldownPath) {
42
+ try { return JSON.parse(readFileSync(cooldownPath, 'utf8')); } catch { return {}; }
32
43
  }
33
44
 
34
- function writeCooldown(data) {
45
+ function writeCooldown(cooldownPath, data, isSessionScoped) {
35
46
  try {
36
47
  mkdirSync(RUNTIME_DIR, { recursive: true });
37
- // Clean stale entries
48
+ // Legacy (no session_id): stale entries trimmed to 10m window.
49
+ // Session-scoped: keep all entries for the session's lifetime — same-file-twice
50
+ // in one session never re-injects. Old session files GC'd on next write.
51
+ const now = Date.now();
52
+ const cleaned = isSessionScoped ? data : {};
53
+ if (!isSessionScoped) {
54
+ for (const [k, v] of Object.entries(data)) {
55
+ if (now - v < STALE_MS) cleaned[k] = v;
56
+ }
57
+ }
58
+ writeFileSync(cooldownPath, JSON.stringify(cleaned));
59
+ } catch { /* silent */ }
60
+ }
61
+
62
+ // Best-effort GC for session cooldown files older than 24h.
63
+ // Runs at most once per hook invocation, silent on any failure.
64
+ function gcOldSessionCooldowns() {
65
+ try {
38
66
  const now = Date.now();
39
- const cleaned = {};
40
- for (const [k, v] of Object.entries(data)) {
41
- if (now - v < STALE_MS) cleaned[k] = v;
67
+ for (const name of readdirSync(RUNTIME_DIR)) {
68
+ if (!name.startsWith('pre-recall-cooldown-') || !name.endsWith('.json')) continue;
69
+ try {
70
+ const p = join(RUNTIME_DIR, name);
71
+ const st = statSync(p);
72
+ if (now - st.mtimeMs > SESSION_COOLDOWN_STALE_MS) unlinkSync(p);
73
+ } catch { /* silent per-entry */ }
42
74
  }
43
- writeFileSync(COOLDOWN_PATH, JSON.stringify(cleaned));
44
75
  } catch { /* silent */ }
45
76
  }
46
77
 
@@ -59,19 +90,29 @@ try {
59
90
 
60
91
  // Parse event
61
92
  let filePath;
93
+ let sessionId;
62
94
  try {
63
95
  const event = JSON.parse(input);
64
96
  filePath = event.tool_input?.file_path;
97
+ sessionId = event.session_id || null;
65
98
  } catch { process.exit(0); }
66
99
 
67
100
  if (!filePath) process.exit(0);
68
101
 
69
- // Cooldown check (full path as key)
70
- const cooldown = readCooldown();
102
+ // v2.33.1: session-scoped cooldown. Within one session, same file recalls
103
+ // once; cross-session, each session gets fresh nudges. Legacy 5-min global
104
+ // cooldown only applies when no session_id is present.
105
+ const cooldownPath = cooldownPathFor(sessionId);
106
+ const isSessionScoped = Boolean(sessionId);
107
+ const cooldown = readCooldown(cooldownPath);
71
108
  const now = Date.now();
72
- if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) {
73
- process.exit(0);
109
+ if (isSessionScoped) {
110
+ if (cooldown[filePath]) process.exit(0); // already recalled this file in-session
111
+ } else {
112
+ if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) process.exit(0);
74
113
  }
114
+ // Best-effort GC of old session cooldown files (cheap, once per invocation)
115
+ if (isSessionScoped) gcOldSessionCooldowns();
75
116
 
76
117
  // Open DB readonly
77
118
  const Database = (await import('better-sqlite3')).default;
@@ -177,7 +218,7 @@ try {
177
218
  }));
178
219
  // Cooldown applies to BOTH branches so the reminder doesn't spam every Edit.
179
220
  cooldown[filePath] = now;
180
- writeCooldown(cooldown);
221
+ writeCooldown(cooldownPath, cooldown, isSessionScoped);
181
222
  } catch {
182
223
  // Silent failure — never block editing
183
224
  } finally {
File without changes
@@ -34,6 +34,22 @@ const BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN || 1e-5);
34
34
  // survive `shouldSkip` but carry too few tokens to justify an FTS lookup.
35
35
  const PROMPT_MIN_LENGTH = 15;
36
36
 
37
+ // v2.33.1: follow-up prompts ("前面那个", "继续 X", "再看看 Y") are short by
38
+ // nature but semantically depend on prior turns. Once a session has injected
39
+ // memory at least once, relax gates so short follow-ups still get recall.
40
+ // Detection: INJECTED_IDS_FILE count > 0 within DEDUP_STALE_MS window.
41
+ const FOLLOWUP_PROMPT_MIN_LENGTH = 8;
42
+ const FOLLOWUP_BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN_FOLLOWUP || 5e-6);
43
+
44
+ function isFollowUpSession() {
45
+ try {
46
+ const raw = readFileSync(INJECTED_IDS_FILE, 'utf8');
47
+ const { ts, count = 0 } = JSON.parse(raw);
48
+ if (!ts || Date.now() - ts > DEDUP_STALE_MS) return false;
49
+ return count > 0;
50
+ } catch { return false; }
51
+ }
52
+
37
53
  // ─── DB Query Functions ─────────────────────────────────────────────────────
38
54
 
39
55
  function searchByFts(db, queryText, project, limit, typeFilter) {
@@ -255,7 +271,12 @@ async function main() {
255
271
  // T3 (v2.31): additional raw-length gate on top of shouldSkip's CJK-weighted
256
272
  // effective-length check. Suppresses medium-short Latin prompts ("run tests",
257
273
  // "fix bug now") that carry too few content tokens for a meaningful FTS lookup.
258
- if (promptText.trim().length < PROMPT_MIN_LENGTH) return;
274
+ // v2.33.1: follow-up prompts in an already-active session get a lower gate —
275
+ // short continuations ("前面那个?", "does it work?") depend on prior context.
276
+ const followUp = isFollowUpSession();
277
+ const promptMinLen = followUp ? FOLLOWUP_PROMPT_MIN_LENGTH : PROMPT_MIN_LENGTH;
278
+ if (promptText.trim().length < promptMinLen) return;
279
+ const bm25Floor = followUp ? FOLLOWUP_BM25_MIN_SCORE : BM25_MIN_SCORE;
259
280
 
260
281
  let db;
261
282
  try {
@@ -275,7 +296,7 @@ async function main() {
275
296
  const errSig = extractErrorSignature(promptText);
276
297
  const sigRows = errSig
277
298
  ? searchByFts(db, errSig.signature, project, 2, 'bugfix').filter(r =>
278
- typeof r.relevance === 'number' && Math.abs(r.relevance) >= BM25_MIN_SCORE
299
+ typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
279
300
  )
280
301
  : [];
281
302
 
@@ -299,7 +320,7 @@ async function main() {
299
320
  // no relevance and are always kept — file-scoped recall is presumed
300
321
  // intentional and has its own relevance signal (the file name match).
301
322
  ftsRows = ftsRows.filter(r =>
302
- typeof r.relevance === 'number' && Math.abs(r.relevance) >= BM25_MIN_SCORE
323
+ typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
303
324
  );
304
325
 
305
326
  // Merge: FTS results first, then file results, deduplicated
package/secret-scrub.mjs CHANGED
File without changes
File without changes
package/server.mjs CHANGED
File without changes
package/skill.md CHANGED
File without changes
package/skip-tools.mjs CHANGED
File without changes
package/source-files.mjs CHANGED
File without changes
package/stop-words.mjs CHANGED
File without changes
package/synonyms.mjs CHANGED
File without changes
package/tfidf.mjs CHANGED
File without changes
package/tier.mjs CHANGED
File without changes
package/tool-schemas.mjs CHANGED
File without changes
package/utils.mjs CHANGED
File without changes