claude-mem-lite 2.24.2 → 2.26.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.
Files changed (58) 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/bash-utils.mjs +0 -0
  8. package/cli.mjs +16 -1
  9. package/commands/mem.md +0 -0
  10. package/commands/memory.md +0 -0
  11. package/commands/recall.md +0 -0
  12. package/commands/recent.md +0 -0
  13. package/commands/search.md +0 -0
  14. package/commands/timeline.md +0 -0
  15. package/commands/tools.md +0 -0
  16. package/commands/update.md +0 -0
  17. package/format-utils.mjs +0 -0
  18. package/haiku-client.mjs +0 -0
  19. package/hash-utils.mjs +0 -0
  20. package/hook-context.mjs +0 -0
  21. package/hook-episode.mjs +0 -1
  22. package/hook-handoff.mjs +0 -0
  23. package/hook-llm.mjs +5 -4
  24. package/hook-memory.mjs +0 -0
  25. package/hook-semaphore.mjs +0 -0
  26. package/hook-shared.mjs +0 -0
  27. package/hook-update.mjs +0 -0
  28. package/hook.mjs +98 -36
  29. package/hooks/hooks.json +12 -0
  30. package/install-metadata.mjs +0 -0
  31. package/install.mjs +13 -8
  32. package/mem-cli.mjs +40 -7
  33. package/nlp.mjs +0 -0
  34. package/package.json +2 -1
  35. package/project-utils.mjs +0 -0
  36. package/registry/preinstalled.json +0 -0
  37. package/registry-indexer.mjs +0 -0
  38. package/registry-retriever.mjs +0 -0
  39. package/registry-scanner.mjs +0 -0
  40. package/registry.mjs +0 -0
  41. package/resource-discovery.mjs +0 -0
  42. package/schema.mjs +0 -0
  43. package/scoring-sql.mjs +0 -0
  44. package/scripts/launch.mjs +0 -0
  45. package/scripts/pre-tool-recall.js +126 -0
  46. package/scripts/prompt-search-utils.mjs +0 -0
  47. package/scripts/user-prompt-search.js +0 -0
  48. package/secret-scrub.mjs +0 -0
  49. package/server-internals.mjs +0 -0
  50. package/server.mjs +2 -1
  51. package/skill.md +0 -0
  52. package/skip-tools.mjs +0 -0
  53. package/stop-words.mjs +0 -0
  54. package/synonyms.mjs +0 -0
  55. package/tfidf.mjs +0 -0
  56. package/tier.mjs +0 -0
  57. package/tool-schemas.mjs +0 -0
  58. package/utils.mjs +0 -0
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.24.2",
13
+ "version": "2.26.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.26.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/.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/bash-utils.mjs CHANGED
File without changes
package/cli.mjs CHANGED
@@ -21,6 +21,21 @@ if (cmd === '--version' || cmd === '-v') {
21
21
  await main(process.argv.slice(2));
22
22
  } else {
23
23
  process.stderr.write(`[mem] Unknown command: "${cmd}"\n`);
24
- process.stderr.write('[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n');
24
+ // Suggest closest command by edit distance
25
+ const allCmds = [...CLI_COMMANDS, ...INSTALL_COMMANDS];
26
+ let best = null, bestDist = Infinity;
27
+ for (const c of allCmds) {
28
+ const a = cmd.toLowerCase(), b = c;
29
+ const m = a.length, n = b.length;
30
+ if (Math.abs(m - n) > 2) continue;
31
+ const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
32
+ for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) d[i][j] = Math.min(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0));
33
+ if (d[m][n] < bestDist) { bestDist = d[m][n]; best = c; }
34
+ }
35
+ if (best && bestDist <= 2) {
36
+ process.stderr.write(`[mem] Did you mean: ${best}?\n`);
37
+ } else {
38
+ process.stderr.write('[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n');
39
+ }
25
40
  process.exitCode = 1;
26
41
  }
package/commands/mem.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/commands/tools.md CHANGED
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
@@ -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-handoff.mjs CHANGED
File without changes
package/hook-llm.mjs CHANGED
@@ -63,7 +63,7 @@ export function saveObservation(obs, projectOverride, sessionIdOverride, externa
63
63
  // "Error in X", "Modified X" titles are low-specificity → use longer dedup window
64
64
  // 7-day exact match prevents cross-day accumulation of "Modified package.json" noise;
65
65
  // 3-day Jaccard catches near-duplicates without blocking legitimately new observations
66
- const LOW_SIGNAL = /^(Error (while working|in)|Modified |Worked on |Reviewed \d+ files:)/;
66
+ const LOW_SIGNAL = /^(Error (while working|in)|Error: |Modified |Worked on |Reviewed \d+ files:|# |node |npm |npx |\(no description\)|\(error\)$)/;
67
67
  if (obs.title && LOW_SIGNAL.test(obs.title)) {
68
68
  const sevenDaysAgo = now.getTime() - 7 * 86400000;
69
69
  const threeDaysAgo = now.getTime() - 3 * 86400000;
@@ -264,9 +264,10 @@ export function buildDegradedTitle(episode) {
264
264
  if (hasEdit) return `Modified ${names}${suffix}`;
265
265
  return `Worked on ${names}${suffix}`;
266
266
  }
267
- // No files: strip raw JSON output from Bash descriptions
267
+ // No files: strip raw output (JSON, arrays, long tails) from Bash descriptions
268
268
  const desc = episode.entries[0]?.desc || '(no description)';
269
- return desc.replace(/ → (?:ERROR: )?\{.*$/, hasError ? ' (error)' : '');
269
+ return desc.replace(/ → (?:ERROR: )?[\[{].*$/, hasError ? ' (error)' : '')
270
+ .replace(/ → .*---EXIT:\d+$/, hasError ? ' (error)' : '');
270
271
  }
271
272
 
272
273
  /**
@@ -300,7 +301,7 @@ export function buildImmediateObservation(episode) {
300
301
  const ruleImportance = computeRuleImportance(episode);
301
302
  // Low-signal degraded titles ("Error in...", "Modified...") should not inflate importance.
302
303
  // Cap at 1 unless rule-based signals indicate genuine importance (error-in-test → 3, config → 2).
303
- const LOW_SIGNAL = /^(Error (while working|in)|Modified |Worked on |Reviewed \d+ files:)/;
304
+ const LOW_SIGNAL = /^(Error (while working|in)|Error: |Modified |Worked on |Reviewed \d+ files:|# |node |npm |npx |\(no description\)|\(error\)$)/;
304
305
  const isLowSignal = LOW_SIGNAL.test(title);
305
306
  let importance;
306
307
  if (isReviewPattern) {
package/hook-memory.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
@@ -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
 
@@ -466,7 +445,7 @@ async function handleSessionStart() {
466
445
  }
467
446
  })();
468
447
 
469
- // Auto-purge: delete stale observations daily (COMPRESSED_PENDING_PURGE, 7-day retention)
448
+ // Auto-maintain: cleanup + decay + boost + purge, gated to once per 24h
470
449
  const maintainFile = join(RUNTIME_DIR, 'last-auto-maintain.json');
471
450
  let shouldMaintain = true;
472
451
  try {
@@ -475,13 +454,69 @@ async function handleSessionStart() {
475
454
  } catch {}
476
455
  if (shouldMaintain) {
477
456
  try {
457
+ const STALE_AGE = Date.now() - 30 * 86400000;
458
+ const OP_CAP = 500;
459
+
460
+ // Purge FIRST: delete entries already marked pending-purge from previous cycles (7-day retention)
461
+ // Must run before decay/idle-mark to avoid same-cycle delete of newly-marked entries
478
462
  const purged = db.prepare(`
479
463
  DELETE FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE}
480
464
  AND created_at_epoch < ?
481
465
  `).run(Date.now() - 7 * 86400000);
482
- if (purged.changes > 0) {
483
- debugLog('DEBUG', 'session-start', `auto-purged ${purged.changes} stale observations`);
484
- }
466
+ if (purged.changes > 0) debugLog('DEBUG', 'auto-maintain', `purged ${purged.changes} stale observations`);
467
+
468
+ // Cleanup: remove broken observations (no title AND no narrative)
469
+ const cleaned = db.prepare(`
470
+ DELETE FROM observations WHERE id IN (
471
+ SELECT id FROM observations
472
+ WHERE COALESCE(compressed_into, 0) = 0
473
+ AND (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
474
+ LIMIT ${OP_CAP}
475
+ )
476
+ `).run();
477
+ if (cleaned.changes > 0) debugLog('DEBUG', 'auto-maintain', `cleaned ${cleaned.changes} broken observations`);
478
+
479
+ // Decay: reduce importance of old, never-accessed observations
480
+ const decayed = db.prepare(`
481
+ UPDATE observations SET importance = MAX(1, COALESCE(importance, 1) - 1)
482
+ WHERE id IN (
483
+ SELECT id FROM observations
484
+ WHERE COALESCE(compressed_into, 0) = 0
485
+ AND COALESCE(importance, 1) > 1
486
+ AND COALESCE(access_count, 0) = 0
487
+ AND created_at_epoch < ?
488
+ LIMIT ${OP_CAP}
489
+ )
490
+ `).run(STALE_AGE);
491
+ if (decayed.changes > 0) debugLog('DEBUG', 'auto-maintain', `decayed ${decayed.changes} stale observations`);
492
+
493
+ // Mark idle: importance=1, never-accessed, old → pending-purge (will be purged next cycle)
494
+ const idleMarked = db.prepare(`
495
+ UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
496
+ WHERE id IN (
497
+ SELECT id FROM observations
498
+ WHERE COALESCE(compressed_into, 0) = 0
499
+ AND COALESCE(importance, 1) = 1
500
+ AND COALESCE(access_count, 0) = 0
501
+ AND created_at_epoch < ?
502
+ LIMIT ${OP_CAP}
503
+ )
504
+ `).run(STALE_AGE);
505
+ if (idleMarked.changes > 0) debugLog('DEBUG', 'auto-maintain', `marked ${idleMarked.changes} idle as pending-purge`);
506
+
507
+ // Boost: increase importance of frequently-accessed observations
508
+ const boosted = db.prepare(`
509
+ UPDATE observations SET importance = MIN(3, COALESCE(importance, 1) + 1)
510
+ WHERE id IN (
511
+ SELECT id FROM observations
512
+ WHERE COALESCE(compressed_into, 0) = 0
513
+ AND COALESCE(access_count, 0) > 3
514
+ AND COALESCE(importance, 1) < 3
515
+ LIMIT ${OP_CAP}
516
+ )
517
+ `).run();
518
+ if (boosted.changes > 0) debugLog('DEBUG', 'auto-maintain', `boosted ${boosted.changes} frequently-accessed observations`);
519
+
485
520
  // Mark maintenance as done (24h gate) — even though compression runs in background
486
521
  writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
487
522
  // Weekly summary grouping runs in background to avoid blocking SessionStart
@@ -650,23 +685,50 @@ async function handleSessionStart() {
650
685
  const summaryLines = buildSummaryLines(latestSummary);
651
686
 
652
687
  // Key context: top high-importance observations for CLAUDE.md persistence
688
+ // Split into "File Lessons" (actionable, has lesson + file) and "Key Context" (informational)
653
689
  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
690
+ SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
691
+ WHERE o.project = ? AND COALESCE(o.compressed_into, 0) = 0
692
+ AND o.superseded_at IS NULL
693
+ AND COALESCE(o.importance, 1) >= 2
694
+ ORDER BY o.created_at_epoch DESC LIMIT 10
658
695
  `).all(project);
696
+
659
697
  if (keyObs.length > 0) {
660
- summaryLines.push('### Key Context');
698
+ const fileLessons = [];
699
+ const keyContext = [];
700
+
661
701
  for (const o of keyObs) {
662
- // Strip raw JSON output from degraded Bash-style titles
663
702
  const clean = (o.title || '(untitled)')
664
703
  .replace(/ → (?:ERROR: )?\{".*$/, '')
665
704
  .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}`);
705
+ const hasLesson = o.lesson_learned && o.lesson_learned.trim();
706
+ const hasFiles = o.files_modified && o.files_modified !== '[]';
707
+
708
+ if (hasLesson && hasFiles) {
709
+ try {
710
+ const files = JSON.parse(o.files_modified);
711
+ const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
712
+ if (fname) {
713
+ fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
714
+ continue;
715
+ }
716
+ } catch {}
717
+ }
718
+ const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
719
+ keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
720
+ }
721
+
722
+ if (fileLessons.length > 0) {
723
+ summaryLines.push('### File Lessons');
724
+ summaryLines.push(...fileLessons.slice(0, 5));
725
+ summaryLines.push('');
726
+ }
727
+ if (keyContext.length > 0) {
728
+ summaryLines.push('### Key Context');
729
+ summaryLines.push(...keyContext.slice(0, 5));
730
+ summaryLines.push('');
668
731
  }
669
- summaryLines.push('');
670
732
  } else if (!latestSummary) {
671
733
  // Fallback: no summary AND no key observations — show recent activity
672
734
  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": "*",
File without changes
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/mem-cli.mjs CHANGED
@@ -82,8 +82,14 @@ function cmdSearch(db, args) {
82
82
  return;
83
83
  }
84
84
 
85
- const limit = Math.max(1, parseInt(flags.limit, 10) || 20);
85
+ const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
86
+ const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 20;
86
87
  const type = flags.type || null;
88
+ const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
89
+ if (type && !validObsTypes.has(type)) {
90
+ fail(`[mem] Invalid --type "${type}". Valid: ${[...validObsTypes].join(', ')}`);
91
+ return;
92
+ }
87
93
  const source = flags.source || null; // observations|sessions|prompts (null = all)
88
94
  const project = flags.project ? resolveProject(db, flags.project) : null;
89
95
  const dateFrom = flags.from ? new Date(flags.from).getTime() : null;
@@ -91,10 +97,18 @@ function cmdSearch(db, args) {
91
97
  if (dateTo && flags.to && /^\d{4}-\d{2}-\d{2}$/.test(flags.to)) dateTo += 86400000 - 1;
92
98
  if (flags.from && isNaN(dateFrom)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
93
99
  if (flags.to && isNaN(dateTo)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
94
- const minImportance = flags.importance ? parseInt(flags.importance, 10) : null;
100
+ const minImportance = flags.importance !== undefined ? parseInt(flags.importance, 10) : null;
101
+ if (minImportance !== null && (isNaN(minImportance) || minImportance < 1 || minImportance > 3)) {
102
+ fail(`[mem] Invalid --importance "${flags.importance}". Must be 1, 2, or 3.`);
103
+ return;
104
+ }
95
105
  const branch = flags.branch || null;
96
106
  const offset = Math.max(0, parseInt(flags.offset, 10) || 0);
97
107
  const tier = flags.tier || null;
108
+ if (tier && !['working', 'active', 'archive'].includes(tier)) {
109
+ fail(`[mem] Invalid --tier "${tier}". Use: working, active, archive`);
110
+ return;
111
+ }
98
112
  const sort = flags.sort || 'relevance';
99
113
  if (!['relevance', 'time', 'importance'].includes(sort)) {
100
114
  fail(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
@@ -305,7 +319,8 @@ function cmdSearch(db, args) {
305
319
  }
306
320
 
307
321
  const showTime = sort === 'time';
308
- out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}":`);
322
+ const hasMixed = paged.some(r => r._source === 'session' || r._source === 'prompt');
323
+ out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}":${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
309
324
  for (const r of paged) {
310
325
  const timeStr = showTime && r.created_at_epoch ? ` (${relativeTime(r.created_at_epoch)})` : '';
311
326
  if (r._source === 'session') {
@@ -464,7 +479,8 @@ function cmdRecall(db, args) {
464
479
  }
465
480
 
466
481
  const filename = basename(file);
467
- const limit = Math.max(1, parseInt(flags.limit, 10) || 10);
482
+ const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
483
+ const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 10;
468
484
 
469
485
  // Search via observation_files junction table for indexed filename lookups
470
486
  const escaped = filename.replace(/%/g, '\\%').replace(/_/g, '\\_');
@@ -549,7 +565,19 @@ function cmdGet(db, args) {
549
565
 
550
566
  // Default: observations (aligned with MCP mem_get)
551
567
  const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
552
- const requestedFields = flags.fields ? flags.fields.split(',').map(s => s.trim()).filter(f => OBS_FIELDS.includes(f)) : null;
568
+ let requestedFields = null;
569
+ if (flags.fields) {
570
+ const allRequested = flags.fields.split(',').map(s => s.trim());
571
+ const invalid = allRequested.filter(f => !OBS_FIELDS.includes(f));
572
+ if (invalid.length > 0) {
573
+ process.stderr.write(`[mem] Unknown field(s): ${invalid.join(', ')}. Valid: ${OBS_FIELDS.join(', ')}\n`);
574
+ }
575
+ requestedFields = allRequested.filter(f => OBS_FIELDS.includes(f));
576
+ if (requestedFields.length === 0) {
577
+ fail('[mem] No valid fields specified');
578
+ return;
579
+ }
580
+ }
553
581
 
554
582
  // Update access_count + auto-boost (aligned with MCP mem_get)
555
583
  db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
@@ -616,6 +644,9 @@ function cmdTimeline(db, args) {
616
644
 
617
645
  // No anchor: show most recent observations (aligned with MCP mem_timeline fallback)
618
646
  if (!anchorId || isNaN(anchorId)) {
647
+ if (queryStr) {
648
+ process.stderr.write(`[mem] No anchor found for "${queryStr}", showing recent timeline\n`);
649
+ }
619
650
  const compressedFilter = 'COALESCE(compressed_into, 0) = 0';
620
651
  const projectFilter = project ? `WHERE ${compressedFilter} AND project = ?` : `WHERE ${compressedFilter}`;
621
652
  const fallbackParams = project ? [project, before + after + 1] : [before + after + 1];
@@ -952,7 +983,8 @@ function cmdBrowse(db, args) {
952
983
  fail(`[mem] Invalid tier: "${tierFilter}". Use: working, active, or archive`);
953
984
  return;
954
985
  }
955
- const limit = Math.max(1, parseInt(flags.limit, 10) || (tierFilter ? 20 : 5));
986
+ const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
987
+ const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : (tierFilter ? 20 : 5);
956
988
  const now = Date.now();
957
989
 
958
990
  const ctx = {
@@ -1185,7 +1217,8 @@ function cmdExport(db, args) {
1185
1217
  wheres.push('created_at_epoch <= ?'); params.push(epoch);
1186
1218
  }
1187
1219
 
1188
- const limit = Math.min(Math.max(1, parseInt(flags.limit, 10) || 200), 1000);
1220
+ const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
1221
+ const limit = Math.min(Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 200, 1000);
1189
1222
  const format = flags.format || 'json';
1190
1223
  if (!['json', 'jsonl'].includes(format)) {
1191
1224
  fail(`[mem] Invalid format "${format}". Use: json or jsonl`);
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.24.2",
3
+ "version": "2.26.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",
package/project-utils.mjs CHANGED
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
@@ -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
+ }
File without changes
File without changes
package/secret-scrub.mjs CHANGED
File without changes
File without changes
package/server.mjs CHANGED
@@ -488,7 +488,8 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCros
488
488
  const countLabel = isCrossSource && totalCount > paginatedResults.length
489
489
  ? `${paginatedResults.length} of ${totalCount}`
490
490
  : `${paginatedResults.length}`;
491
- lines.push(`Found ${countLabel} result(s)${args.query ? ` for "${args.query}"` : ''}:\n`);
491
+ const hasMixed = paginatedResults.some(r => r.source === 'session' || r.source === 'prompt');
492
+ lines.push(`Found ${countLabel} result(s)${args.query ? ` for "${args.query}"` : ''}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}\n`);
492
493
 
493
494
  for (const r of paginatedResults) {
494
495
  if (r.source === 'obs') {
package/skill.md CHANGED
File without changes
package/skip-tools.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