claude-mem-lite 2.24.0 → 2.24.2

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.0",
13
+ "version": "2.24.2",
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.0",
3
+ "version": "2.24.2",
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/install.mjs CHANGED
@@ -977,11 +977,11 @@ async function doctor() {
977
977
  issues++;
978
978
  }
979
979
 
980
- const mcpPath = join(INSTALL_DIR, 'node_modules', '@modelcontextprotocol');
981
- if (existsSync(mcpPath)) {
982
- ok('@modelcontextprotocol/sdk: installed');
983
- } else {
984
- fail('@modelcontextprotocol/sdk: not installed');
980
+ try {
981
+ await import('@modelcontextprotocol/sdk/server/mcp.js');
982
+ ok('@modelcontextprotocol/sdk: verified (import OK)');
983
+ } catch (e) {
984
+ fail(`@modelcontextprotocol/sdk: import failed (${e.message})`);
985
985
  issues++;
986
986
  }
987
987
 
package/mem-cli.mjs CHANGED
@@ -26,7 +26,7 @@ function parseArgs(argv) {
26
26
  if (arg.startsWith('--')) {
27
27
  const key = arg.slice(2);
28
28
  const next = argv[i + 1];
29
- if (next !== undefined && !next.startsWith('--') && !next.startsWith('-')) {
29
+ if (next !== undefined && !next.startsWith('--') && (!next.startsWith('-') || /^-\d/.test(next))) {
30
30
  flags[key] = next;
31
31
  i += 2;
32
32
  } else {
@@ -50,6 +50,11 @@ function out(text) {
50
50
  process.stdout.write(text + '\n');
51
51
  }
52
52
 
53
+ function fail(text) {
54
+ process.stdout.write(text + '\n');
55
+ process.exitCode = 1;
56
+ }
57
+
53
58
  function relativeTime(epochMs) {
54
59
  const diff = Date.now() - epochMs;
55
60
  if (diff < 0) return 'just now';
@@ -73,7 +78,7 @@ function cmdSearch(db, args) {
73
78
  const { positional, flags } = parseArgs(args);
74
79
  const query = positional.join(' ');
75
80
  if (!query) {
76
- out('[mem] Usage: mem search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance]');
81
+ fail('[mem] Usage: mem search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance]');
77
82
  return;
78
83
  }
79
84
 
@@ -84,29 +89,35 @@ function cmdSearch(db, args) {
84
89
  const dateFrom = flags.from ? new Date(flags.from).getTime() : null;
85
90
  let dateTo = flags.to ? new Date(flags.to).getTime() : null;
86
91
  if (dateTo && flags.to && /^\d{4}-\d{2}-\d{2}$/.test(flags.to)) dateTo += 86400000 - 1;
87
- if (flags.from && isNaN(dateFrom)) { out(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
88
- if (flags.to && isNaN(dateTo)) { out(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
92
+ if (flags.from && isNaN(dateFrom)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
93
+ if (flags.to && isNaN(dateTo)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
89
94
  const minImportance = flags.importance ? parseInt(flags.importance, 10) : null;
90
95
  const branch = flags.branch || null;
91
96
  const offset = Math.max(0, parseInt(flags.offset, 10) || 0);
92
97
  const tier = flags.tier || null;
93
98
  const sort = flags.sort || 'relevance';
94
99
  if (!['relevance', 'time', 'importance'].includes(sort)) {
95
- out(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
100
+ fail(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
96
101
  return;
97
102
  }
98
103
 
99
104
  if (source && !['observations', 'sessions', 'prompts'].includes(source)) {
100
- out(`[mem] Invalid --source "${source}". Use: observations, sessions, prompts`);
105
+ fail(`[mem] Invalid --source "${source}". Use: observations, sessions, prompts`);
101
106
  return;
102
107
  }
103
108
 
104
109
  const ftsQuery = sanitizeFtsQuery(query);
105
110
  if (!ftsQuery) {
106
- out(`[mem] No valid search terms in "${query}"`);
111
+ fail(`[mem] No valid search terms in "${query}"`);
107
112
  return;
108
113
  }
109
114
 
115
+ // Warn if obs-only filters used with non-observation source
116
+ if (source && source !== 'observations' && (type || tier || minImportance)) {
117
+ const ignored = [type && '--type', tier && '--tier', minImportance && '--importance'].filter(Boolean);
118
+ process.stderr.write(`[mem] Note: ${ignored.join(', ')} only apply to observations, ignored for --source ${source}\n`);
119
+ }
120
+
110
121
  // When --type/--tier/--importance (obs-only fields) is specified, implicitly restrict to observations
111
122
  const effectiveSource = source || ((type || tier || minImportance) ? 'observations' : null);
112
123
 
@@ -211,7 +222,7 @@ function cmdSearch(db, args) {
211
222
  sessParams.push(effectiveSource ? limit : limit, effectiveSource ? offset : 0);
212
223
  try {
213
224
  const sessRows = db.prepare(`
214
- SELECT s.id, s.request, s.completed, s.project, s.created_at,
225
+ SELECT s.id, s.request, s.completed, s.project, s.created_at, s.created_at_epoch,
215
226
  ${SESS_BM25}
216
227
  * (1.0 + EXP(-0.693 * (? - s.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
217
228
  * (CASE WHEN ? IS NOT NULL AND s.project = ? THEN 2.0 ELSE 1.0 END) as score
@@ -235,7 +246,7 @@ function cmdSearch(db, args) {
235
246
  promptParams.push(effectiveSource ? limit : limit, effectiveSource ? offset : 0);
236
247
  try {
237
248
  const promptRows = db.prepare(`
238
- SELECT p.id, p.prompt_text, p.content_session_id, p.created_at,
249
+ SELECT p.id, p.prompt_text, p.content_session_id, p.created_at, p.created_at_epoch,
239
250
  bm25(user_prompts_fts, 1) as score
240
251
  FROM user_prompts_fts
241
252
  JOIN user_prompts p ON user_prompts_fts.rowid = p.id
@@ -285,22 +296,29 @@ function cmdSearch(db, args) {
285
296
  }
286
297
  // else 'relevance' keeps BM25 score order (already sorted)
287
298
 
288
- // Cross-source: trim to limit with offset
289
- const paged = effectiveSource ? results : results.slice(offset, offset + limit);
299
+ // Trim to limit with offset
300
+ const paged = results.slice(offset, offset + limit);
290
301
 
302
+ if (paged.length === 0) {
303
+ out(`[mem] No results for "${query}" at offset ${offset}`);
304
+ return;
305
+ }
306
+
307
+ const showTime = sort === 'time';
291
308
  out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}":`);
292
309
  for (const r of paged) {
310
+ const timeStr = showTime && r.created_at_epoch ? ` (${relativeTime(r.created_at_epoch)})` : '';
293
311
  if (r._source === 'session') {
294
312
  const date = fmtDateShort(r.created_at);
295
- out(`S#${r.id} 📋 ${date} ${truncate(r.request || r.completed || '(no summary)', 80)}`);
313
+ out(`S#${r.id} 📋 ${date}${timeStr} ${truncate(r.request || r.completed || '(no summary)', 80)}`);
296
314
  } else if (r._source === 'prompt') {
297
315
  const date = fmtDateShort(r.created_at);
298
- out(`P#${r.id} 💬 ${date} ${truncate(r.prompt_text || '(empty)', 80)}`);
316
+ out(`P#${r.id} 💬 ${date}${timeStr} ${truncate(r.prompt_text || '(empty)', 80)}`);
299
317
  } else {
300
318
  const date = fmtDateShort(r.created_at);
301
319
  const title = truncate(r.title || r.subtitle || '(untitled)', 80);
302
320
  const supersededTag = r.superseded ? ' [SUPERSEDED]' : '';
303
- out(`#${r.id} ${typeIcon(r.type)} ${date} ${title}${supersededTag}`);
321
+ out(`#${r.id} ${typeIcon(r.type)} ${date}${timeStr} ${title}${supersededTag}`);
304
322
  if (r.lesson_learned) {
305
323
  out(` -> ${truncate(r.lesson_learned, 80)}`);
306
324
  }
@@ -333,7 +351,7 @@ function searchFts(db, ftsQuery, { type, project, limit, dateFrom, dateTo, minIm
333
351
 
334
352
  // Scoring aligned with server.mjs: BM25 × type-decay × type-quality × project_boost × importance × access_bonus
335
353
  const ftsRows = db.prepare(`
336
- SELECT o.id, o.type, o.title, o.subtitle, o.created_at, o.lesson_learned,
354
+ SELECT o.id, o.type, o.title, o.subtitle, o.created_at, o.created_at_epoch, o.lesson_learned,
337
355
  o.files_modified, o.importance,
338
356
  ${OBS_BM25}
339
357
  * (1.0 + EXP(-0.693 * (? - MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))) / ${TYPE_DECAY_CASE}))
@@ -407,7 +425,8 @@ function searchFts(db, ftsQuery, { type, project, limit, dateFrom, dateTo, minIm
407
425
 
408
426
  function cmdRecent(db, args) {
409
427
  const { positional, flags } = parseArgs(args);
410
- const limit = Math.max(1, parseInt(positional[0], 10) || 10);
428
+ const rawLimit = parseInt(positional[0], 10);
429
+ const limit = Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 10);
411
430
  const project = flags.project ? resolveProject(db, flags.project) : inferProject();
412
431
 
413
432
  const params = [];
@@ -440,7 +459,7 @@ function cmdRecall(db, args) {
440
459
  const { positional, flags } = parseArgs(args);
441
460
  const file = positional.join(' ');
442
461
  if (!file) {
443
- out('[mem] Usage: mem recall <file>');
462
+ fail('[mem] Usage: mem recall <file>');
444
463
  return;
445
464
  }
446
465
 
@@ -483,13 +502,13 @@ function cmdGet(db, args) {
483
502
  const { positional, flags } = parseArgs(args);
484
503
  const idStr = positional.join(',');
485
504
  if (!idStr) {
486
- out('[mem] Usage: mem get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]');
505
+ fail('[mem] Usage: mem get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]');
487
506
  return;
488
507
  }
489
508
 
490
509
  const ids = idStr.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
491
510
  if (ids.length === 0) {
492
- out('[mem] No valid IDs provided');
511
+ fail('[mem] No valid IDs provided');
493
512
  return;
494
513
  }
495
514
 
@@ -498,7 +517,7 @@ function cmdGet(db, args) {
498
517
 
499
518
  if (source === 'session') {
500
519
  const rows = db.prepare(`SELECT * FROM session_summaries WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...ids);
501
- if (rows.length === 0) { out('[mem] No sessions found for given IDs'); return; }
520
+ if (rows.length === 0) { fail('[mem] No sessions found for given IDs'); return; }
502
521
  const parts = [];
503
522
  for (const r of rows) {
504
523
  const lines = [`S#${r.id} ${fmtDateShort(r.created_at)}`];
@@ -516,7 +535,7 @@ function cmdGet(db, args) {
516
535
 
517
536
  if (source === 'prompt') {
518
537
  const rows = db.prepare(`SELECT * FROM user_prompts WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...ids);
519
- if (rows.length === 0) { out('[mem] No prompts found for given IDs'); return; }
538
+ if (rows.length === 0) { fail('[mem] No prompts found for given IDs'); return; }
520
539
  const parts = [];
521
540
  for (const r of rows) {
522
541
  const lines = [`P#${r.id} ${fmtDateShort(r.created_at)}`];
@@ -543,7 +562,7 @@ function cmdGet(db, args) {
543
562
  `).all(...ids);
544
563
 
545
564
  if (rows.length === 0) {
546
- out('[mem] No observations found for given IDs');
565
+ fail('[mem] No observations found for given IDs');
547
566
  return;
548
567
  }
549
568
 
@@ -627,7 +646,7 @@ function cmdTimeline(db, args) {
627
646
  // Get anchor epoch
628
647
  const anchorRow = db.prepare('SELECT created_at_epoch, project FROM observations WHERE id = ?').get(anchorId);
629
648
  if (!anchorRow) {
630
- out(`[mem] Observation #${anchorId} not found`);
649
+ fail(`[mem] Observation #${anchorId} not found`);
631
650
  return;
632
651
  }
633
652
 
@@ -672,20 +691,25 @@ function cmdSave(db, args) {
672
691
  const { positional, flags } = parseArgs(args);
673
692
  const text = positional.join(' ');
674
693
  if (!text) {
675
- out('[mem] Usage: mem save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2]');
694
+ fail('[mem] Usage: mem save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2]');
676
695
  return;
677
696
  }
678
697
 
679
698
  const type = flags.type || 'discovery';
680
699
  const validTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
681
700
  if (!validTypes.has(type)) {
682
- out(`[mem] Invalid type "${type}". Valid: ${[...validTypes].join(', ')}`);
701
+ fail(`[mem] Invalid type "${type}". Valid: ${[...validTypes].join(', ')}`);
683
702
  return;
684
703
  }
685
704
 
686
705
  const rawTitle = flags.title || text.slice(0, 100);
687
706
  // Explicit saves default to importance=2 (notable) — user chose to save
688
- const importance = Math.max(1, Math.min(3, parseInt(flags.importance, 10) || 2));
707
+ const rawImp = flags.importance !== undefined ? parseInt(flags.importance, 10) : 2;
708
+ if (flags.importance !== undefined && (isNaN(rawImp) || rawImp < 1 || rawImp > 3)) {
709
+ fail(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
710
+ return;
711
+ }
712
+ const importance = rawImp;
689
713
  const project = flags.project ? resolveProject(db, flags.project) : inferProject();
690
714
  const saveFiles = flags.files ? flags.files.split(',').map(f => f.trim()).filter(Boolean) : [];
691
715
 
@@ -831,7 +855,8 @@ function cmdStats(db, args) {
831
855
  const tdParams = tierSqlParams(tierCtx);
832
856
  const tierDist = db.prepare(`
833
857
  SELECT tier, COUNT(*) as c FROM (
834
- SELECT ${TIER_CASE_SQL} as tier FROM observations WHERE 1=1 ${projectFilter}
858
+ SELECT ${TIER_CASE_SQL} as tier FROM observations
859
+ WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
835
860
  ) GROUP BY tier ORDER BY tier
836
861
  `).all(...tdParams, ...baseParams);
837
862
  const tierMap = Object.fromEntries(tierDist.map(r => [r.tier, r.c]));
@@ -924,7 +949,7 @@ function cmdBrowse(db, args) {
924
949
  const project = flags.project ? resolveProject(db, flags.project) : inferProject();
925
950
  const tierFilter = flags.tier || null;
926
951
  if (tierFilter && !['working', 'active', 'archive'].includes(tierFilter)) {
927
- out(`[mem] Invalid tier: "${tierFilter}". Use: working, active, or archive`);
952
+ fail(`[mem] Invalid tier: "${tierFilter}". Use: working, active, or archive`);
928
953
  return;
929
954
  }
930
955
  const limit = Math.max(1, parseInt(flags.limit, 10) || (tierFilter ? 20 : 5));
@@ -1007,13 +1032,13 @@ function cmdDelete(db, args) {
1007
1032
  const { positional, flags } = parseArgs(args);
1008
1033
  const idStr = positional.join(',');
1009
1034
  if (!idStr) {
1010
- out('[mem] Usage: mem delete <id1,id2,...> [--confirm]');
1035
+ fail('[mem] Usage: mem delete <id1,id2,...> [--confirm]');
1011
1036
  return;
1012
1037
  }
1013
1038
 
1014
1039
  const ids = idStr.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
1015
1040
  if (ids.length === 0) {
1016
- out('[mem] No valid IDs provided');
1041
+ fail('[mem] No valid IDs provided');
1017
1042
  return;
1018
1043
  }
1019
1044
 
@@ -1022,7 +1047,7 @@ function cmdDelete(db, args) {
1022
1047
  const rows = db.prepare(`SELECT id, type, title, project FROM observations WHERE id IN (${placeholders})`).all(...ids);
1023
1048
 
1024
1049
  if (rows.length === 0) {
1025
- out('[mem] No observations found for given IDs');
1050
+ fail('[mem] No observations found for given IDs');
1026
1051
  return;
1027
1052
  }
1028
1053
 
@@ -1066,24 +1091,24 @@ function cmdUpdate(db, args) {
1066
1091
  const { positional, flags } = parseArgs(args);
1067
1092
  const id = parseInt(positional[0], 10);
1068
1093
  if (!id || isNaN(id)) {
1069
- out('[mem] Usage: mem update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1094
+ fail('[mem] Usage: mem update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1070
1095
  return;
1071
1096
  }
1072
1097
 
1073
1098
  const obs = db.prepare('SELECT id, title FROM observations WHERE id = ?').get(id);
1074
1099
  if (!obs) {
1075
- out(`[mem] Observation #${id} not found`);
1100
+ fail(`[mem] Observation #${id} not found`);
1076
1101
  return;
1077
1102
  }
1078
1103
 
1079
1104
  const updates = [];
1080
1105
  const params = [];
1081
- if (flags.title) { updates.push('title = ?'); params.push(scrubSecrets(flags.title)); }
1082
- if (flags.narrative) { updates.push('narrative = ?'); params.push(scrubSecrets(flags.narrative)); }
1106
+ if (flags.title !== undefined) { updates.push('title = ?'); params.push(scrubSecrets(flags.title)); }
1107
+ if (flags.narrative !== undefined) { updates.push('narrative = ?'); params.push(scrubSecrets(flags.narrative)); }
1083
1108
  if (flags.type) {
1084
1109
  const validTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
1085
1110
  if (!validTypes.has(flags.type)) {
1086
- out(`[mem] Invalid type "${flags.type}". Valid: ${[...validTypes].join(', ')}`);
1111
+ fail(`[mem] Invalid type "${flags.type}". Valid: ${[...validTypes].join(', ')}`);
1087
1112
  return;
1088
1113
  }
1089
1114
  updates.push('type = ?'); params.push(flags.type);
@@ -1091,16 +1116,16 @@ function cmdUpdate(db, args) {
1091
1116
  if (flags.importance) {
1092
1117
  const imp = parseInt(flags.importance, 10);
1093
1118
  if (isNaN(imp) || imp < 1 || imp > 3) {
1094
- out(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
1119
+ fail(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
1095
1120
  return;
1096
1121
  }
1097
1122
  updates.push('importance = ?'); params.push(imp);
1098
1123
  }
1099
- if (flags.lesson || flags['lesson-learned']) { updates.push('lesson_learned = ?'); params.push(scrubSecrets(flags.lesson || flags['lesson-learned'])); }
1100
- if (flags.concepts) { updates.push('concepts = ?'); params.push(flags.concepts); }
1124
+ if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) { updates.push('lesson_learned = ?'); params.push(scrubSecrets(flags.lesson ?? flags['lesson-learned'] ?? '')); }
1125
+ if (flags.concepts !== undefined) { updates.push('concepts = ?'); params.push(flags.concepts); }
1101
1126
 
1102
1127
  if (updates.length === 0) {
1103
- out('[mem] No fields to update. Use --title, --type, --importance, --lesson/--lesson-learned, --narrative, --concepts');
1128
+ fail('[mem] No fields to update. Use --title, --type, --importance, --lesson/--lesson-learned, --narrative, --concepts');
1104
1129
  return;
1105
1130
  }
1106
1131
 
@@ -1150,12 +1175,12 @@ function cmdExport(db, args) {
1150
1175
  if (flags.type) { wheres.push('type = ?'); params.push(flags.type); }
1151
1176
  if (flags.from) {
1152
1177
  const epoch = new Date(flags.from).getTime();
1153
- if (isNaN(epoch)) { out(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
1178
+ if (isNaN(epoch)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
1154
1179
  wheres.push('created_at_epoch >= ?'); params.push(epoch);
1155
1180
  }
1156
1181
  if (flags.to) {
1157
1182
  let epoch = new Date(flags.to).getTime();
1158
- if (isNaN(epoch)) { out(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
1183
+ if (isNaN(epoch)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
1159
1184
  if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) epoch += 86400000 - 1;
1160
1185
  wheres.push('created_at_epoch <= ?'); params.push(epoch);
1161
1186
  }
@@ -1163,7 +1188,7 @@ function cmdExport(db, args) {
1163
1188
  const limit = Math.min(Math.max(1, parseInt(flags.limit, 10) || 200), 1000);
1164
1189
  const format = flags.format || 'json';
1165
1190
  if (!['json', 'jsonl'].includes(format)) {
1166
- out(`[mem] Invalid format "${format}". Use: json or jsonl`);
1191
+ fail(`[mem] Invalid format "${format}". Use: json or jsonl`);
1167
1192
  return;
1168
1193
  }
1169
1194
 
@@ -1292,7 +1317,7 @@ function cmdMaintain(db, args) {
1292
1317
  const { positional, flags } = parseArgs(args);
1293
1318
  const action = positional[0];
1294
1319
  if (!action || !['scan', 'execute'].includes(action)) {
1295
- out('[mem] Usage: mem maintain <scan|execute> [--ops cleanup,decay,boost,dedup,purge_stale,rebuild_vectors] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...]');
1320
+ fail('[mem] Usage: mem maintain <scan|execute> [--ops cleanup,decay,boost,dedup,purge_stale,rebuild_vectors] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...]');
1296
1321
  return;
1297
1322
  }
1298
1323
 
@@ -1353,7 +1378,7 @@ function cmdMaintain(db, args) {
1353
1378
  out(` Stale (>30d, imp=1, no access): ${stats.stale}`);
1354
1379
  out(` Broken (no title/narrative): ${stats.broken}`);
1355
1380
  out(` Boostable (accessed>3, imp<3): ${stats.boostable}`);
1356
- out(` Pending purge: ${pendingPurge.count}`);
1381
+ out(` Pending purge: ${pendingPurge.count} (compressed originals awaiting cleanup)`);
1357
1382
  if (duplicates.length > 0) {
1358
1383
  const AUTO_MERGE_THRESHOLD = 0.85;
1359
1384
  const autoMergeable = duplicates.filter(d => parseFloat(d.similarity) >= AUTO_MERGE_THRESHOLD);
@@ -1390,7 +1415,7 @@ function cmdMaintain(db, args) {
1390
1415
  const ops = opsStr.split(',').map(s => s.trim());
1391
1416
  const invalidOps = ops.filter(op => !VALID_OPS.includes(op));
1392
1417
  if (invalidOps.length > 0) {
1393
- out(`[mem] Unknown operation(s): ${invalidOps.join(', ')}. Valid: ${VALID_OPS.join(', ')}`);
1418
+ fail(`[mem] Unknown operation(s): ${invalidOps.join(', ')}. Valid: ${VALID_OPS.join(', ')}`);
1394
1419
  return;
1395
1420
  }
1396
1421
  const staleAge = Date.now() - STALE_AGE_MS;
@@ -1527,7 +1552,7 @@ function cmdFtsCheck(db, args) {
1527
1552
  const { positional } = parseArgs(args);
1528
1553
  const action = positional[0];
1529
1554
  if (!action || !['check', 'rebuild'].includes(action)) {
1530
- out('[mem] Usage: mem fts-check <check|rebuild>');
1555
+ fail('[mem] Usage: mem fts-check <check|rebuild>');
1531
1556
  return;
1532
1557
  }
1533
1558
 
@@ -1558,7 +1583,7 @@ function cmdRegistry(_memDb, args) {
1558
1583
  const { positional, flags } = parseArgs(args);
1559
1584
  const action = positional[0];
1560
1585
  if (!action || !['list', 'stats', 'search', 'import', 'remove', 'reindex'].includes(action)) {
1561
- out('[mem] Usage: mem registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1586
+ fail('[mem] Usage: mem registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1562
1587
  return;
1563
1588
  }
1564
1589
 
@@ -1574,7 +1599,7 @@ function cmdRegistry(_memDb, args) {
1574
1599
  try {
1575
1600
  if (action === 'search') {
1576
1601
  const query = flags.query || positional.slice(1).join(' ');
1577
- if (!query) { out('[mem] Usage: mem registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1602
+ if (!query) { fail('[mem] Usage: mem registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1578
1603
  let results = searchResources(rdb, query, {
1579
1604
  type: flags.type || undefined,
1580
1605
  limit: (flags.category || flags.quality) ? 20 : 10,
@@ -1646,7 +1671,7 @@ function cmdRegistry(_memDb, args) {
1646
1671
  if (action === 'import') {
1647
1672
  const name = flags.name;
1648
1673
  const resourceType = flags['resource-type'];
1649
- if (!name || !resourceType) { out('[mem] Usage: mem registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1674
+ if (!name || !resourceType) { fail('[mem] Usage: mem registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1650
1675
  const fields = { name, type: resourceType, status: 'active', source: flags.source || 'user' };
1651
1676
  for (const f of ['repo-url', 'local-path', 'invocation-name', 'intent-tags', 'domain-tags', 'trigger-patterns', 'capability-summary', 'keywords', 'tech-stack', 'use-cases']) {
1652
1677
  const camel = f.replace(/-([a-z])/g, (_, c) => '_' + c);
@@ -1667,7 +1692,7 @@ function cmdRegistry(_memDb, args) {
1667
1692
  if (action === 'remove') {
1668
1693
  const name = flags.name;
1669
1694
  const resourceType = flags['resource-type'];
1670
- if (!name || !resourceType) { out('[mem] Usage: mem registry remove --name N --resource-type skill|agent'); return; }
1695
+ if (!name || !resourceType) { fail('[mem] Usage: mem registry remove --name N --resource-type skill|agent'); return; }
1671
1696
  const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
1672
1697
  out(result.changes > 0 ? `[mem] Removed: ${resourceType}:${name}` : '[mem] Not found.');
1673
1698
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.24.0",
3
+ "version": "2.24.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,6 +20,28 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
20
20
  process.stderr.write('[claude-mem-lite] Dependencies installed\n');
21
21
  }
22
22
 
23
+ // Verify MCP SDK is importable (exports mapping intact).
24
+ // Incomplete installs can leave the directory present but package.json missing,
25
+ // causing Node.js to fail resolving subpath exports like /server/mcp.js.
26
+ try {
27
+ await import('@modelcontextprotocol/sdk/server/mcp.js');
28
+ } catch (firstErr) {
29
+ process.stderr.write(`[claude-mem-lite] MCP SDK broken (${firstErr.code || firstErr.message}) — reinstalling...\n`);
30
+ try {
31
+ execSync('npm install @modelcontextprotocol/sdk --force --omit=dev --no-audit --no-fund', {
32
+ cwd: ROOT,
33
+ stdio: ['ignore', 'pipe', 'inherit'],
34
+ timeout: 60_000,
35
+ });
36
+ // Verify the reinstall actually fixed it
37
+ await import('@modelcontextprotocol/sdk/server/mcp.js');
38
+ process.stderr.write('[claude-mem-lite] MCP SDK repaired\n');
39
+ } catch (e) {
40
+ process.stderr.write(`[claude-mem-lite] MCP SDK repair failed: ${e.message}\n`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
23
45
  // Dev mode: prefer ~/.claude-mem-lite/server.mjs (symlinked to source) over
24
46
  // CLAUDE_PLUGIN_ROOT (potentially stale plugin cache). This ensures the MCP
25
47
  // server always runs the latest code when installed with `install --dev`.
package/scripts/setup.sh CHANGED
@@ -122,7 +122,11 @@ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
122
122
  CACHE_DIR="$HOME/.claude/plugins/cache/sdsrss/claude-mem-lite"
123
123
  if [[ -d "$CACHE_DIR" ]]; then
124
124
  # List version dirs sorted by semver descending, skip top 3
125
- mapfile -t OLD_VERS < <(ls -1 "$CACHE_DIR" | grep -E '^[0-9]+\.' | sort -t. -k1,1nr -k2,2nr -k3,3nr | tail -n +4)
125
+ # Use while-read instead of mapfile for bash 3.2 (macOS) compatibility
126
+ OLD_VERS=()
127
+ while IFS= read -r ver; do
128
+ [[ -n "$ver" ]] && OLD_VERS+=("$ver")
129
+ done < <(ls -1 "$CACHE_DIR" | grep -E '^[0-9]+\.' | sort -t. -k1,1nr -k2,2nr -k3,3nr | tail -n +4)
126
130
  if [[ ${#OLD_VERS[@]} -gt 0 ]]; then
127
131
  for ver in "${OLD_VERS[@]}"; do
128
132
  rm -rf "${CACHE_DIR:?}/$ver" 2>/dev/null || true
package/server.mjs CHANGED
@@ -1064,7 +1064,7 @@ server.registerTool(
1064
1064
  const tierDist = db.prepare(`
1065
1065
  SELECT tier, COUNT(*) as c FROM (
1066
1066
  SELECT ${TIER_CASE_SQL} as tier FROM observations
1067
- WHERE 1=1 ${projectFilter}
1067
+ WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
1068
1068
  ) GROUP BY tier ORDER BY tier
1069
1069
  `).all(...tdParams, ...baseParams);
1070
1070
  const tierMap = Object.fromEntries(tierDist.map(r => [r.tier, r.c]));