claude-mem-lite 2.63.0 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.63.0",
13
+ "version": "2.65.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.63.0",
3
+ "version": "2.65.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/cli/fts-check.mjs CHANGED
@@ -8,7 +8,7 @@ export function cmdFtsCheck(db, args) {
8
8
  const { positional } = parseArgs(args);
9
9
  const action = positional[0];
10
10
  if (!action || !['check', 'rebuild'].includes(action)) {
11
- fail('[mem] Usage: mem fts-check <check|rebuild>');
11
+ fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
12
12
  return;
13
13
  }
14
14
 
package/install.mjs CHANGED
@@ -1295,14 +1295,29 @@ async function doctor() {
1295
1295
  dwarn('Database: not found (will be created)');
1296
1296
  }
1297
1297
 
1298
- // Check for stale processes
1298
+ // Check for stale processes — extends beyond legacy chroma/worker to
1299
+ // catch MCP launchers / servers from cached old plugin versions. Auto-update
1300
+ // bumps installed_plugins.json but cannot kill the MCP process spawned for
1301
+ // an active session, so v2.60.0/v2.61.0 launchers commonly outlive their
1302
+ // version (recurrent pattern, see #2580 for the gsd analogue). Filtering
1303
+ // strategy: legacy chroma/worker = always stale; cache-path launchers = only
1304
+ // when their version segment ≠ current package.json version; dev-install
1305
+ // paths (no version segment) are never flagged.
1299
1306
  try {
1300
- const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
1301
- // Filter out the pgrep process itself (matches its own pattern)
1302
- const real = procs.split('\n').filter(l => !l.includes('pgrep'));
1303
- if (real.length > 0) {
1304
- warn('Old processes running:\n ' + real.join('\n '));
1307
+ const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem-lite.*(scripts/launch|server)\\.mjs|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
1308
+ const lines = procs.split('\n').filter(l => l && !l.includes('pgrep'));
1309
+ let currentVersion = '';
1310
+ try { currentVersion = JSON.parse(readFileSync(join(PROJECT_DIR, 'package.json'), 'utf8')).version; } catch { /* fall through with empty version */ }
1311
+ const stale = lines.filter(l => {
1312
+ if (/chroma|claude-mem.*worker/.test(l)) return true;
1313
+ const m = l.match(/claude-mem-lite\/(\d+\.\d+\.\d+)\/(scripts\/launch|server)\.mjs/);
1314
+ return m && currentVersion && m[1] !== currentVersion;
1315
+ });
1316
+ if (stale.length > 0) {
1317
+ warn(`Old processes running${currentVersion ? ` (current: v${currentVersion})` : ''}:\n ` + stale.join('\n '));
1305
1318
  issues++;
1319
+ } else {
1320
+ ok('No stale processes');
1306
1321
  }
1307
1322
  } catch {
1308
1323
  ok('No stale processes');
@@ -1333,19 +1348,31 @@ async function doctor() {
1333
1348
  }
1334
1349
 
1335
1350
  // Dev drift: in dev-mode installs, all SOURCE_FILES entries should be
1336
- // symlinks. A plain file means an earlier install (or manual cp) copied it,
1337
- // so edits in the repo won't propagate to INSTALL_DIR hook runtime and
1338
- // test runtime silently diverge.
1351
+ // symlinks. A plain file means an earlier install (or manual cp) copied it
1352
+ // (edits in the repo won't propagate). A missing entry (neither symlink nor
1353
+ // plain) means an earlier install never wrote the file — same divergence
1354
+ // class. Per #8043: "is this file present ≠ is this install consistent" —
1355
+ // missing is tracked separately by checkDevDrift but the caller MUST surface
1356
+ // it to honour #8268's "gate the all-green string on every counter" rule.
1339
1357
  try {
1340
1358
  const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
1341
1359
  const r = checkDevDrift(INSTALL_DIR, SOURCE_FILES);
1342
- if (r.drift) {
1343
- const names = r.details.join(', ');
1344
- const suffix = r.plainCount > r.details.length ? ` +${r.plainCount - r.details.length} more` : '';
1345
- warn(`Dev drift: ${r.plainCount} non-symlink file(s) in dev install: ${names}${suffix} (re-run: node ${join(PROJECT_DIR, 'install.mjs')} install --dev)`);
1360
+ if (r.drift || (r.devMode && r.missingCount > 0)) {
1361
+ const parts = [];
1362
+ if (r.plainCount > 0) {
1363
+ const names = r.plainFiles.slice(0, 5).join(', ');
1364
+ const suffix = r.plainCount > 5 ? ` +${r.plainCount - 5} more` : '';
1365
+ parts.push(`${r.plainCount} non-symlink: ${names}${suffix}`);
1366
+ }
1367
+ if (r.missingCount > 0) {
1368
+ const names = r.missingFiles.join(', ');
1369
+ const suffix = r.missingCount > r.missingFiles.length ? ` +${r.missingCount - r.missingFiles.length} more` : '';
1370
+ parts.push(`${r.missingCount} missing: ${names}${suffix}`);
1371
+ }
1372
+ warn(`Dev drift: ${parts.join('; ')} (re-run: node ${join(PROJECT_DIR, 'install.mjs')} install --dev)`);
1346
1373
  issues++;
1347
1374
  } else if (r.devMode) {
1348
- ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain)`);
1375
+ ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain, 0 missing)`);
1349
1376
  }
1350
1377
  // Prod (all plain) install: no message — dev-drift is a dev-only concern.
1351
1378
  } catch (e) {
@@ -41,6 +41,7 @@ export function checkDevDrift(installDir, sourceFiles) {
41
41
  plainCount: plainFiles.length,
42
42
  plainFiles,
43
43
  missingCount: missing.length,
44
+ missingFiles: missing.slice(0, 5),
44
45
  details: plainFiles.slice(0, 5),
45
46
  };
46
47
  }
@@ -1,5 +1,5 @@
1
1
  // Shared "save one observation" pipeline — used by both mem-cli.mjs::cmdSave
2
- // (CLI `mem save`) and server.mjs::mem_save (MCP tool).
2
+ // (CLI `claude-mem-lite save`) and server.mjs::mem_save (MCP tool).
3
3
  //
4
4
  // Pre-extraction (v2.60.0) the same dedup → scrub → minhash → CJK-bigram →
5
5
  // transactional INSERT block lived inline in both call sites (~110 lines × 2,
package/mem-cli.mjs CHANGED
@@ -34,12 +34,17 @@ function cmdSearch(db, args) {
34
34
  const { positional, flags } = parseArgs(args);
35
35
  const query = positional.join(' ');
36
36
  if (!query) {
37
- 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] [--include-noise]');
37
+ fail('[mem] Usage: claude-mem-lite search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance] [--include-noise]');
38
38
  return;
39
39
  }
40
40
 
41
41
  const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
42
- const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 20;
42
+ // Distinguish missing/non-integer (use default) from non-positive (silently clamping to 1
43
+ // produced confusing "Found 1 of 44 result" output for --limit 0/-N — warn instead).
44
+ if (flags.limit !== undefined && (!Number.isInteger(rawLimit) || rawLimit < 1)) {
45
+ process.stderr.write(`[mem] Invalid --limit "${flags.limit}" (must be a positive integer); using default 20\n`);
46
+ }
47
+ const limit = Number.isInteger(rawLimit) && rawLimit >= 1 ? rawLimit : 20;
43
48
  const type = flags.type || null;
44
49
  const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
45
50
  if (type && !validObsTypes.has(type)) {
@@ -53,13 +58,22 @@ function cmdSearch(db, args) {
53
58
  if (dateTo && flags.to && /^\d{4}-\d{2}-\d{2}$/.test(flags.to)) dateTo += 86400000 - 1;
54
59
  if (flags.from && isNaN(dateFrom)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
55
60
  if (flags.to && isNaN(dateTo)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
61
+ // Inverted range silently returns 0 rows; warn so users see the cause, don't error
62
+ // (a deliberate "search for nothing in this window" is not malformed input).
63
+ if (dateFrom !== null && dateTo !== null && dateFrom > dateTo) {
64
+ process.stderr.write(`[mem] Note: --from "${flags.from}" is after --to "${flags.to}"; this range is empty\n`);
65
+ }
56
66
  const minImportance = flags.importance !== undefined ? parseInt(flags.importance, 10) : null;
57
67
  if (minImportance !== null && (isNaN(minImportance) || minImportance < 1 || minImportance > 3)) {
58
68
  fail(`[mem] Invalid --importance "${flags.importance}". Must be 1, 2, or 3.`);
59
69
  return;
60
70
  }
61
71
  const branch = flags.branch || null;
62
- const offset = Math.max(0, parseInt(flags.offset, 10) || 0);
72
+ const rawOffset = flags.offset !== undefined ? parseInt(flags.offset, 10) : NaN;
73
+ if (flags.offset !== undefined && (!Number.isInteger(rawOffset) || rawOffset < 0)) {
74
+ process.stderr.write(`[mem] Invalid --offset "${flags.offset}" (must be a non-negative integer); using 0\n`);
75
+ }
76
+ const offset = Number.isInteger(rawOffset) && rawOffset >= 0 ? rawOffset : 0;
63
77
  const tier = flags.tier || null;
64
78
  if (tier && !['working', 'active', 'archive'].includes(tier)) {
65
79
  fail(`[mem] Invalid --tier "${tier}". Use: working, active, archive`);
@@ -338,7 +352,9 @@ function cmdSearch(db, args) {
338
352
  }
339
353
 
340
354
  const countLabel = total > paged.length ? `${paged.length} of ${total}` : `${paged.length}`;
341
- out(`[mem] Found ${countLabel} result${paged.length !== 1 ? 's' : ''} for "${query}"${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
355
+ // Pluralize on total "Found 1 of 44 result" reads wrong; the population (44) drives
356
+ // grammatical number, not the page slice (1).
357
+ out(`[mem] Found ${countLabel} result${total !== 1 ? 's' : ''} for "${query}"${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
342
358
  for (const r of paged) {
343
359
  const timeStr = showTime && r.created_at_epoch ? ` (${relativeTime(r.created_at_epoch)})` : '';
344
360
  if (r._source === 'session') {
@@ -400,13 +416,16 @@ function cmdRecall(db, args) {
400
416
  const { positional, flags } = parseArgs(args);
401
417
  const file = positional.join(' ');
402
418
  if (!file) {
403
- fail('[mem] Usage: mem recall <file> [--limit N] [--include-noise]');
419
+ fail('[mem] Usage: claude-mem-lite recall <file> [--limit N] [--include-noise]');
404
420
  return;
405
421
  }
406
422
 
407
423
  const filename = basename(file);
408
424
  const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
409
- const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 10;
425
+ if (flags.limit !== undefined && (!Number.isInteger(rawLimit) || rawLimit < 1)) {
426
+ process.stderr.write(`[mem] Invalid --limit "${flags.limit}" (must be a positive integer); using default 10\n`);
427
+ }
428
+ const limit = Number.isInteger(rawLimit) && rawLimit >= 1 ? rawLimit : 10;
410
429
  const includeNoise = flags['include-noise'] === true || flags['include-noise'] === 'true';
411
430
 
412
431
  // Search via observation_files junction table for indexed filename lookups
@@ -530,7 +549,7 @@ function cmdGet(db, args) {
530
549
  const { positional, flags } = parseArgs(args);
531
550
  const idStr = positional.join(',');
532
551
  if (!idStr) {
533
- fail('[mem] Usage: mem get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
552
+ fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
534
553
  ' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session).');
535
554
  return;
536
555
  }
@@ -602,8 +621,19 @@ function cmdGet(db, args) {
602
621
 
603
622
  function cmdTimeline(db, args) {
604
623
  const { positional, flags } = parseArgs(args);
605
- const before = parseInt(flags.before, 10) || 5;
606
- const after = parseInt(flags.after, 10) || 5;
624
+ // parseInt('-5') === -5 is truthy, so `|| 5` doesn't rescue negative input.
625
+ // Match cmdSearch's warn-then-default pattern for consistency across CLI flags.
626
+ const parseWindow = (label, raw) => {
627
+ if (raw === undefined) return 5;
628
+ const n = parseInt(raw, 10);
629
+ if (!Number.isInteger(n) || n < 0) {
630
+ process.stderr.write(`[mem] Invalid --${label} "${raw}" (must be a non-negative integer); using default 5\n`);
631
+ return 5;
632
+ }
633
+ return n;
634
+ };
635
+ const before = parseWindow('before', flags.before);
636
+ const after = parseWindow('after', flags.after);
607
637
  const project = flags.project ? resolveProject(db, flags.project) : null;
608
638
 
609
639
  // Parse --anchor, accepting P#/S#/# prefix so callers can paste search-result IDs verbatim.
@@ -785,7 +815,7 @@ function cmdSave(db, args) {
785
815
  const { positional, flags } = parseArgs(args);
786
816
  const text = positional.join(' ');
787
817
  if (!text) {
788
- fail('[mem] Usage: mem save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
818
+ fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
789
819
  return;
790
820
  }
791
821
 
@@ -967,6 +997,9 @@ async function cmdStats(db, args) {
967
997
  const compressedCount = db.prepare(
968
998
  `SELECT COUNT(*) as c FROM observations WHERE compressed_into IS NOT NULL ${projectFilter}`
969
999
  ).get(...baseParams);
1000
+ const supersededOnlyCount = db.prepare(
1001
+ `SELECT COUNT(*) as c FROM observations WHERE superseded_at IS NOT NULL AND compressed_into IS NULL ${projectFilter}`
1002
+ ).get(...baseParams);
970
1003
 
971
1004
  // Tier distribution (aligned with MCP mem_stats)
972
1005
  const tierCtx = { now, currentProject: project || inferProject(), currentSessionId: '' };
@@ -1005,7 +1038,11 @@ async function cmdStats(db, args) {
1005
1038
  out(` Compressed: ${compressedCount.c}`);
1006
1039
  if (noiseRatio > 0.6) out(' ⚠️ High noise ratio — consider running mem compress');
1007
1040
  out('');
1008
- out('Tier distribution:');
1041
+ // Tier counts only live (uncompressed, non-superseded) observations — surface the
1042
+ // full decomposition so live + compressed + superseded = Total adds up cleanly.
1043
+ const tierTotal = (tierMap.working ?? 0) + (tierMap.active ?? 0) + (tierMap.archive ?? 0);
1044
+ const supersededLabel = supersededOnlyCount.c > 0 ? ` + ${supersededOnlyCount.c} superseded` : '';
1045
+ out(`Tier distribution (live ${tierTotal}, excludes ${compressedCount.c} compressed${supersededLabel}):`);
1009
1046
  out(` 🔴 Working: ${tierMap.working ?? 0} | 🟡 Active: ${tierMap.active ?? 0} | 🔵 Archive: ${tierMap.archive ?? 0}`);
1010
1047
  }
1011
1048
 
@@ -1137,7 +1174,7 @@ function cmdDelete(db, args) {
1137
1174
  const { positional, flags } = parseArgs(args);
1138
1175
  const idStr = positional.join(',');
1139
1176
  if (!idStr) {
1140
- fail('[mem] Usage: mem delete <id1,id2,...> [--confirm]');
1177
+ fail('[mem] Usage: claude-mem-lite delete <id1,id2,...> [--confirm]');
1141
1178
  return;
1142
1179
  }
1143
1180
 
@@ -1147,7 +1184,7 @@ function cmdDelete(db, args) {
1147
1184
  const nonObs = tokens.filter(t => /^[PpSs]#?\d+$/.test(t));
1148
1185
  if (nonObs.length > 0) {
1149
1186
  fail(`[mem] delete only works on observations. Rejected: ${nonObs.join(', ')}. ` +
1150
- `Prompts and sessions are append-only — inspect with \`mem get P#N --source prompt\` / \`--source session\`.`);
1187
+ `Prompts and sessions are append-only — inspect with \`claude-mem-lite get P#N --source prompt\` / \`--source session\`.`);
1151
1188
  return;
1152
1189
  }
1153
1190
  const ids = tokens.map(t => {
@@ -1215,7 +1252,7 @@ function cmdUpdate(db, args) {
1215
1252
  const parsed = raw ? parseIdToken(raw) : null;
1216
1253
  const id = parsed && parsed.source === null ? parsed.id : parseInt(raw, 10);
1217
1254
  if (!id || isNaN(id)) {
1218
- fail('[mem] Usage: mem update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1255
+ fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1219
1256
  return;
1220
1257
  }
1221
1258
 
@@ -1297,16 +1334,21 @@ function cmdExport(db, args) {
1297
1334
  const project = flags.project ? resolveProject(db, flags.project) : null;
1298
1335
  if (project) { wheres.push('project = ?'); params.push(project); }
1299
1336
  if (flags.type) { wheres.push('type = ?'); params.push(flags.type); }
1337
+ let exportFromEpoch = null;
1338
+ let exportToEpoch = null;
1300
1339
  if (flags.from) {
1301
- const epoch = new Date(flags.from).getTime();
1302
- if (isNaN(epoch)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
1303
- wheres.push('created_at_epoch >= ?'); params.push(epoch);
1340
+ exportFromEpoch = new Date(flags.from).getTime();
1341
+ if (isNaN(exportFromEpoch)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
1342
+ wheres.push('created_at_epoch >= ?'); params.push(exportFromEpoch);
1304
1343
  }
1305
1344
  if (flags.to) {
1306
- let epoch = new Date(flags.to).getTime();
1307
- if (isNaN(epoch)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
1308
- if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) epoch += 86400000 - 1;
1309
- wheres.push('created_at_epoch <= ?'); params.push(epoch);
1345
+ exportToEpoch = new Date(flags.to).getTime();
1346
+ if (isNaN(exportToEpoch)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
1347
+ if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) exportToEpoch += 86400000 - 1;
1348
+ wheres.push('created_at_epoch <= ?'); params.push(exportToEpoch);
1349
+ }
1350
+ if (exportFromEpoch !== null && exportToEpoch !== null && exportFromEpoch > exportToEpoch) {
1351
+ process.stderr.write(`[mem] Note: --from "${flags.from}" is after --to "${flags.to}"; this range is empty\n`);
1310
1352
  }
1311
1353
 
1312
1354
  const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
@@ -1442,7 +1484,7 @@ function cmdMaintain(db, args) {
1442
1484
  const { positional, flags } = parseArgs(args);
1443
1485
  const action = positional[0];
1444
1486
  if (!action || !['scan', 'execute'].includes(action)) {
1445
- 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,...]');
1487
+ fail("[mem] Usage: claude-mem-lite maintain <scan|execute> [--ops cleanup,decay,boost,dedup,purge_stale,rebuild_vectors] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...]'scan' previews, 'execute' applies.");
1446
1488
  return;
1447
1489
  }
1448
1490
 
@@ -1722,7 +1764,7 @@ function cmdRegistry(_memDb, args) {
1722
1764
  const { positional, flags } = parseArgs(args);
1723
1765
  const action = positional[0];
1724
1766
  if (!action || !['list', 'stats', 'search', 'import', 'remove', 'reindex'].includes(action)) {
1725
- fail('[mem] Usage: mem registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1767
+ fail('[mem] Usage: claude-mem-lite registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1726
1768
  return;
1727
1769
  }
1728
1770
 
@@ -1738,7 +1780,7 @@ function cmdRegistry(_memDb, args) {
1738
1780
  try {
1739
1781
  if (action === 'search') {
1740
1782
  const query = flags.query || positional.slice(1).join(' ');
1741
- if (!query) { fail('[mem] Usage: mem registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1783
+ if (!query) { fail('[mem] Usage: claude-mem-lite registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1742
1784
  let results = searchResources(rdb, query, {
1743
1785
  type: flags.type || undefined,
1744
1786
  limit: (flags.category || flags.quality) ? 20 : 10,
@@ -1828,7 +1870,7 @@ function cmdRegistry(_memDb, args) {
1828
1870
  if (action === 'import') {
1829
1871
  const name = flags.name;
1830
1872
  const resourceType = flags['resource-type'];
1831
- if (!name || !resourceType) { fail('[mem] Usage: mem registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1873
+ if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1832
1874
  const fields = { name, type: resourceType, status: 'active', source: flags.source || 'user' };
1833
1875
  for (const f of ['repo-url', 'local-path', 'invocation-name', 'intent-tags', 'domain-tags', 'trigger-patterns', 'capability-summary', 'keywords', 'tech-stack', 'use-cases']) {
1834
1876
  const camel = f.replace(/-([a-z])/g, (_, c) => '_' + c);
@@ -1849,7 +1891,7 @@ function cmdRegistry(_memDb, args) {
1849
1891
  if (action === 'remove') {
1850
1892
  const name = flags.name;
1851
1893
  const resourceType = flags['resource-type'];
1852
- if (!name || !resourceType) { fail('[mem] Usage: mem registry remove --name N --resource-type skill|agent'); return; }
1894
+ if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry remove --name N --resource-type skill|agent'); return; }
1853
1895
  const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
1854
1896
  out(result.changes > 0 ? `[mem] Removed: ${resourceType}:${name}` : '[mem] Not found.');
1855
1897
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.63.0",
3
+ "version": "2.65.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
package/server.mjs CHANGED
@@ -1005,6 +1005,9 @@ server.registerTool(
1005
1005
  const compressedCount = db.prepare(`
1006
1006
  SELECT COUNT(*) as c FROM observations WHERE compressed_into IS NOT NULL ${projectFilter}
1007
1007
  `).get(...baseParams);
1008
+ const supersededOnlyCount = db.prepare(`
1009
+ SELECT COUNT(*) as c FROM observations WHERE superseded_at IS NOT NULL AND compressed_into IS NULL ${projectFilter}
1010
+ `).get(...baseParams);
1008
1011
 
1009
1012
  // Tier distribution
1010
1013
  const tierCtx = { now: Date.now(), currentProject: args.project || inferProject(), currentSessionId: '' };
@@ -1038,7 +1041,9 @@ server.registerTool(
1038
1041
  ` Compressed: ${compressedCount.c}`,
1039
1042
  ...(noiseRatio > 0.6 ? [' ⚠️ High noise ratio — consider running mem_compress'] : []),
1040
1043
  '',
1041
- 'Tier distribution:',
1044
+ // Tier counts only live (uncompressed, non-superseded) observations — surface
1045
+ // the full decomposition so live + compressed + superseded = Total adds up cleanly.
1046
+ `Tier distribution (live ${(tierMap.working ?? 0) + (tierMap.active ?? 0) + (tierMap.archive ?? 0)}, excludes ${compressedCount.c} compressed${supersededOnlyCount.c > 0 ? ` + ${supersededOnlyCount.c} superseded` : ''}):`,
1042
1047
  ` 🔴 Working: ${tierMap.working ?? 0} | 🟡 Active: ${tierMap.active ?? 0} | 🔵 Archive: ${tierMap.archive ?? 0}`,
1043
1048
  ];
1044
1049