claude-mem-lite 2.64.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.64.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.64.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/mem-cli.mjs CHANGED
@@ -39,7 +39,12 @@ function cmdSearch(db, args) {
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') {
@@ -406,7 +422,10 @@ function cmdRecall(db, args) {
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
@@ -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.
@@ -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
 
@@ -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: claude-mem-lite 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.64.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