claude-mem-lite 2.25.0 → 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 -0
  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 +60 -4
  29. package/hooks/hooks.json +0 -0
  30. package/install-metadata.mjs +0 -0
  31. package/install.mjs +0 -0
  32. package/mem-cli.mjs +40 -7
  33. package/nlp.mjs +0 -0
  34. package/package.json +1 -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 +0 -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.25.0",
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.25.0",
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
File without changes
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
@@ -445,7 +445,7 @@ async function handleSessionStart() {
445
445
  }
446
446
  })();
447
447
 
448
- // Auto-purge: delete stale observations daily (COMPRESSED_PENDING_PURGE, 7-day retention)
448
+ // Auto-maintain: cleanup + decay + boost + purge, gated to once per 24h
449
449
  const maintainFile = join(RUNTIME_DIR, 'last-auto-maintain.json');
450
450
  let shouldMaintain = true;
451
451
  try {
@@ -454,13 +454,69 @@ async function handleSessionStart() {
454
454
  } catch {}
455
455
  if (shouldMaintain) {
456
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
457
462
  const purged = db.prepare(`
458
463
  DELETE FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE}
459
464
  AND created_at_epoch < ?
460
465
  `).run(Date.now() - 7 * 86400000);
461
- if (purged.changes > 0) {
462
- debugLog('DEBUG', 'session-start', `auto-purged ${purged.changes} stale observations`);
463
- }
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
+
464
520
  // Mark maintenance as done (24h gate) — even though compression runs in background
465
521
  writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
466
522
  // Weekly summary grouping runs in background to avoid blocking SessionStart
package/hooks/hooks.json CHANGED
File without changes
File without changes
package/install.mjs CHANGED
File without changes
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.25.0",
3
+ "version": "2.26.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
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
File without changes
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