claude-mem-lite 2.62.1 → 2.64.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.62.1",
13
+ "version": "2.64.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.62.1",
3
+ "version": "2.64.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
@@ -280,6 +280,25 @@ function ok(msg) { console.log(` ✓ ${msg}`); }
280
280
  function warn(msg) { console.log(` ⚠ ${msg}`); }
281
281
  function fail(msg) { console.log(` ✗ ${msg}`); }
282
282
 
283
+ // Pure JSON-version field bumper for the release pipeline. Reads `filePath`,
284
+ // walks `keyPath` (e.g. `['version']` or `['plugins', 0, 'version']`), and
285
+ // rewrites only when the new value differs. Returns `{ changed, prev }` so
286
+ // callers can log "X → Y" with the captured-before-mutation value — pre-2.63.0
287
+ // the plugin.json branch in syncVersions logged "Y → Y" because it read the
288
+ // field after assignment.
289
+ export function bumpJsonField(filePath, keyPath, newVal) {
290
+ const json = JSON.parse(readFileSync(filePath, 'utf8'));
291
+ let parent = json;
292
+ for (let i = 0; i < keyPath.length - 1; i++) parent = parent?.[keyPath[i]];
293
+ if (!parent) return { changed: false, prev: undefined };
294
+ const lastKey = keyPath[keyPath.length - 1];
295
+ const prev = parent[lastKey];
296
+ if (prev === newVal) return { changed: false, prev };
297
+ parent[lastKey] = newVal;
298
+ writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n');
299
+ return { changed: true, prev };
300
+ }
301
+
283
302
  // Doctor's final summary line. Pure function so the 4-way contract
284
303
  // (clean / warnings-only / issues / mixed) is unit-testable without spinning
285
304
  // up the full doctor pipeline. `issues` are ✗-level (action required);
@@ -1276,14 +1295,29 @@ async function doctor() {
1276
1295
  dwarn('Database: not found (will be created)');
1277
1296
  }
1278
1297
 
1279
- // 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.
1280
1306
  try {
1281
- const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
1282
- // Filter out the pgrep process itself (matches its own pattern)
1283
- const real = procs.split('\n').filter(l => !l.includes('pgrep'));
1284
- if (real.length > 0) {
1285
- 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 '));
1286
1318
  issues++;
1319
+ } else {
1320
+ ok('No stale processes');
1287
1321
  }
1288
1322
  } catch {
1289
1323
  ok('No stale processes');
@@ -1314,19 +1348,31 @@ async function doctor() {
1314
1348
  }
1315
1349
 
1316
1350
  // Dev drift: in dev-mode installs, all SOURCE_FILES entries should be
1317
- // symlinks. A plain file means an earlier install (or manual cp) copied it,
1318
- // so edits in the repo won't propagate to INSTALL_DIR hook runtime and
1319
- // 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.
1320
1357
  try {
1321
1358
  const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
1322
1359
  const r = checkDevDrift(INSTALL_DIR, SOURCE_FILES);
1323
- if (r.drift) {
1324
- const names = r.details.join(', ');
1325
- const suffix = r.plainCount > r.details.length ? ` +${r.plainCount - r.details.length} more` : '';
1326
- 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)`);
1327
1373
  issues++;
1328
1374
  } else if (r.devMode) {
1329
- ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain)`);
1375
+ ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain, 0 missing)`);
1330
1376
  }
1331
1377
  // Prod (all plain) install: no message — dev-drift is a dev-only concern.
1332
1378
  } catch (e) {
@@ -1593,34 +1639,19 @@ function syncVersions() {
1593
1639
  const version = pkg.version;
1594
1640
  log(`package.json version: ${version}`);
1595
1641
 
1596
- // Sync plugin.json
1597
1642
  const pluginJsonPath = join(PROJECT_DIR, '.claude-plugin', 'plugin.json');
1598
1643
  if (existsSync(pluginJsonPath)) {
1599
- const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf8'));
1600
- if (pluginJson.version !== version) {
1601
- pluginJson.version = version;
1602
- writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
1603
- ok(`plugin.json: ${pluginJson.version} → ${version}`);
1604
- } else {
1605
- ok(`plugin.json: already ${version}`);
1606
- }
1644
+ const r = bumpJsonField(pluginJsonPath, ['version'], version);
1645
+ ok(r.changed ? `plugin.json: ${r.prev} → ${version}` : `plugin.json: already ${version}`);
1607
1646
  } else {
1608
1647
  warn('plugin.json not found');
1609
1648
  }
1610
1649
 
1611
- // Sync marketplace.json
1612
1650
  const marketJsonPath = join(PROJECT_DIR, '.claude-plugin', 'marketplace.json');
1613
1651
  if (existsSync(marketJsonPath)) {
1614
- const marketJson = JSON.parse(readFileSync(marketJsonPath, 'utf8'));
1615
- const plugin = marketJson.plugins?.[0];
1616
- if (plugin && plugin.version !== version) {
1617
- const prev = plugin.version;
1618
- plugin.version = version;
1619
- writeFileSync(marketJsonPath, JSON.stringify(marketJson, null, 2) + '\n');
1620
- ok(`marketplace.json: ${prev} → ${version}`);
1621
- } else if (plugin) {
1622
- ok(`marketplace.json: already ${version}`);
1623
- }
1652
+ const r = bumpJsonField(marketJsonPath, ['plugins', 0, 'version'], version);
1653
+ if (r.prev === undefined) warn('marketplace.json: plugins[0] not found');
1654
+ else ok(r.changed ? `marketplace.json: ${r.prev} → ${version}` : `marketplace.json: already ${version}`);
1624
1655
  } else {
1625
1656
  warn('marketplace.json not found');
1626
1657
  }
@@ -1649,6 +1680,29 @@ function syncVersions() {
1649
1680
  console.log('');
1650
1681
  }
1651
1682
 
1683
+ // Regenerate package-lock.json via npm@10.9.2 to guarantee CI parity. The
1684
+ // drift this prevents: `npm install --package-lock-only` on npm@11+ silently
1685
+ // strips top-level `@emnapi/core` + `@emnapi/runtime` entries when those are
1686
+ // transitive deps of platform-optional bindings (e.g. `@oxc-parser/binding-*`
1687
+ // from knip), and CI's bundled npm@10 (Node 22 default in GitHub Actions)
1688
+ // then refuses `npm ci` with EUSAGE. Same recipe bit twice (#8271 / 2.58.2 /
1689
+ // 2.62.1) before this guard. The packageManager field in package.json
1690
+ // declares the same version for corepack-aware tooling. Network cost: ~5-30s
1691
+ // per release; release cadence makes this acceptable.
1692
+ function regenerateLockfile() {
1693
+ console.log('\nclaude-mem-lite release — regenerate lockfile (npm@10.9.2)\n');
1694
+ try {
1695
+ execFileSync('npx', ['--yes', 'npm@10.9.2', 'install'], {
1696
+ stdio: 'inherit',
1697
+ cwd: PROJECT_DIR,
1698
+ });
1699
+ ok('lockfile regenerated');
1700
+ } catch (e) {
1701
+ fail('lockfile regen failed: ' + e.message);
1702
+ throw e;
1703
+ }
1704
+ }
1705
+
1652
1706
  // ─── Main ───────────────────────────────────────────────────────────────────
1653
1707
 
1654
1708
  export async function main(argv = process.argv.slice(2)) {
@@ -1680,6 +1734,7 @@ export async function main(argv = process.argv.slice(2)) {
1680
1734
  break;
1681
1735
  case 'release':
1682
1736
  syncVersions();
1737
+ if (!flags.has('--no-lock')) regenerateLockfile();
1683
1738
  break;
1684
1739
  default:
1685
1740
  if (IS_NPX) {
@@ -1699,7 +1754,7 @@ Usage:
1699
1754
  node install.mjs cleanup Remove stale temp/staging files
1700
1755
  node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
1701
1756
  node install.mjs self-update Check for and install updates
1702
- node install.mjs release Sync version to plugin.json + marketplace.json
1757
+ node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
1703
1758
 
1704
1759
  npx claude-mem-lite Install via npx (one-liner)
1705
1760
  `);
@@ -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,7 +34,7 @@ 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
 
@@ -400,7 +400,7 @@ function cmdRecall(db, args) {
400
400
  const { positional, flags } = parseArgs(args);
401
401
  const file = positional.join(' ');
402
402
  if (!file) {
403
- fail('[mem] Usage: mem recall <file> [--limit N] [--include-noise]');
403
+ fail('[mem] Usage: claude-mem-lite recall <file> [--limit N] [--include-noise]');
404
404
  return;
405
405
  }
406
406
 
@@ -447,6 +447,25 @@ function cmdRecall(db, args) {
447
447
 
448
448
  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'];
449
449
 
450
+ // Integer-typed time-epoch fields on the observations table that the `get`
451
+ // command renders. Callers expect raw ms (audit) AND a relative-time hint
452
+ // (human-scan), so formatObsFieldValue emits both. Other epoch fields like
453
+ // `created_at_epoch` / `optimized_at` / `last_injected_at` aren't in
454
+ // OBS_FIELDS so they don't surface via `get`.
455
+ export const OBS_TIME_FIELDS = ['superseded_at', 'last_accessed_at'];
456
+
457
+ // Pure formatter — null/undefined/non-time pass through; time fields on
458
+ // integer values render as `<raw> (<relative>)` mirroring the convention
459
+ // already used by `recent` / `timeline` / `recall`. Pre-2.63.0 the get
460
+ // path printed bare ms (e.g. `last_accessed_at: 1778357330957`).
461
+ export function formatObsFieldValue(field, val) {
462
+ if (val === null || val === undefined) return val;
463
+ if (OBS_TIME_FIELDS.includes(field) && typeof val === 'number') {
464
+ return `${val} (${relativeTime(val)})`;
465
+ }
466
+ return val;
467
+ }
468
+
450
469
  function renderObsRows(db, ids, requestedFields) {
451
470
  const placeholders = ids.map(() => '?').join(',');
452
471
  try {
@@ -465,8 +484,9 @@ function renderObsRows(db, ids, requestedFields) {
465
484
  const val = r[f];
466
485
  if (val === null || val === undefined || val === '') continue;
467
486
  if (f === 'text' && r.narrative && typeof val === 'string' && val.startsWith(r.narrative)) continue;
487
+ const formatted = formatObsFieldValue(f, val);
468
488
  const maxLen = f === 'narrative' ? 1000 : f === 'lesson_learned' ? 500 : f === 'text' ? 500 : 200;
469
- const display = typeof val === 'string' && val.length > maxLen ? val.slice(0, maxLen) + '…' : val;
489
+ const display = typeof formatted === 'string' && formatted.length > maxLen ? formatted.slice(0, maxLen) + '…' : formatted;
470
490
  lines.push(`${f}: ${display}`);
471
491
  }
472
492
  parts.push(lines.join('\n'));
@@ -510,7 +530,7 @@ function cmdGet(db, args) {
510
530
  const { positional, flags } = parseArgs(args);
511
531
  const idStr = positional.join(',');
512
532
  if (!idStr) {
513
- fail('[mem] Usage: mem get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
533
+ fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
514
534
  ' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session).');
515
535
  return;
516
536
  }
@@ -672,7 +692,12 @@ function cmdTimeline(db, args) {
672
692
  if ((!anchorId || isNaN(anchorId)) && queryStr) {
673
693
  const ftsQuery = sanitizeFtsQuery(queryStr);
674
694
  const found = findFtsAnchor(db, { ftsQuery, project: project ?? null });
675
- if (found) anchorId = found;
695
+ if (found) {
696
+ anchorId = found.id;
697
+ if (found.relaxed && !anchorNote) {
698
+ anchorNote = `(query "${queryStr}" relaxed AND→OR — no row matched all terms)`;
699
+ }
700
+ }
676
701
  }
677
702
 
678
703
  // No anchor: show most recent observations (aligned with MCP mem_timeline fallback)
@@ -760,7 +785,7 @@ function cmdSave(db, args) {
760
785
  const { positional, flags } = parseArgs(args);
761
786
  const text = positional.join(' ');
762
787
  if (!text) {
763
- fail('[mem] Usage: mem save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
788
+ fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
764
789
  return;
765
790
  }
766
791
 
@@ -1112,7 +1137,7 @@ function cmdDelete(db, args) {
1112
1137
  const { positional, flags } = parseArgs(args);
1113
1138
  const idStr = positional.join(',');
1114
1139
  if (!idStr) {
1115
- fail('[mem] Usage: mem delete <id1,id2,...> [--confirm]');
1140
+ fail('[mem] Usage: claude-mem-lite delete <id1,id2,...> [--confirm]');
1116
1141
  return;
1117
1142
  }
1118
1143
 
@@ -1122,7 +1147,7 @@ function cmdDelete(db, args) {
1122
1147
  const nonObs = tokens.filter(t => /^[PpSs]#?\d+$/.test(t));
1123
1148
  if (nonObs.length > 0) {
1124
1149
  fail(`[mem] delete only works on observations. Rejected: ${nonObs.join(', ')}. ` +
1125
- `Prompts and sessions are append-only — inspect with \`mem get P#N --source prompt\` / \`--source session\`.`);
1150
+ `Prompts and sessions are append-only — inspect with \`claude-mem-lite get P#N --source prompt\` / \`--source session\`.`);
1126
1151
  return;
1127
1152
  }
1128
1153
  const ids = tokens.map(t => {
@@ -1190,7 +1215,7 @@ function cmdUpdate(db, args) {
1190
1215
  const parsed = raw ? parseIdToken(raw) : null;
1191
1216
  const id = parsed && parsed.source === null ? parsed.id : parseInt(raw, 10);
1192
1217
  if (!id || isNaN(id)) {
1193
- fail('[mem] Usage: mem update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1218
+ fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
1194
1219
  return;
1195
1220
  }
1196
1221
 
@@ -1417,7 +1442,7 @@ function cmdMaintain(db, args) {
1417
1442
  const { positional, flags } = parseArgs(args);
1418
1443
  const action = positional[0];
1419
1444
  if (!action || !['scan', 'execute'].includes(action)) {
1420
- 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,...]');
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,...]');
1421
1446
  return;
1422
1447
  }
1423
1448
 
@@ -1697,7 +1722,7 @@ function cmdRegistry(_memDb, args) {
1697
1722
  const { positional, flags } = parseArgs(args);
1698
1723
  const action = positional[0];
1699
1724
  if (!action || !['list', 'stats', 'search', 'import', 'remove', 'reindex'].includes(action)) {
1700
- fail('[mem] Usage: mem registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1725
+ fail('[mem] Usage: claude-mem-lite registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
1701
1726
  return;
1702
1727
  }
1703
1728
 
@@ -1713,7 +1738,7 @@ function cmdRegistry(_memDb, args) {
1713
1738
  try {
1714
1739
  if (action === 'search') {
1715
1740
  const query = flags.query || positional.slice(1).join(' ');
1716
- if (!query) { fail('[mem] Usage: mem registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1741
+ if (!query) { fail('[mem] Usage: claude-mem-lite registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
1717
1742
  let results = searchResources(rdb, query, {
1718
1743
  type: flags.type || undefined,
1719
1744
  limit: (flags.category || flags.quality) ? 20 : 10,
@@ -1803,7 +1828,7 @@ function cmdRegistry(_memDb, args) {
1803
1828
  if (action === 'import') {
1804
1829
  const name = flags.name;
1805
1830
  const resourceType = flags['resource-type'];
1806
- if (!name || !resourceType) { fail('[mem] Usage: mem registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1831
+ if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
1807
1832
  const fields = { name, type: resourceType, status: 'active', source: flags.source || 'user' };
1808
1833
  for (const f of ['repo-url', 'local-path', 'invocation-name', 'intent-tags', 'domain-tags', 'trigger-patterns', 'capability-summary', 'keywords', 'tech-stack', 'use-cases']) {
1809
1834
  const camel = f.replace(/-([a-z])/g, (_, c) => '_' + c);
@@ -1824,7 +1849,7 @@ function cmdRegistry(_memDb, args) {
1824
1849
  if (action === 'remove') {
1825
1850
  const name = flags.name;
1826
1851
  const resourceType = flags['resource-type'];
1827
- if (!name || !resourceType) { fail('[mem] Usage: mem registry remove --name N --resource-type skill|agent'); return; }
1852
+ if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry remove --name N --resource-type skill|agent'); return; }
1828
1853
  const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
1829
1854
  out(result.changes > 0 ? `[mem] Removed: ${resourceType}:${name}` : '[mem] Not found.');
1830
1855
  return;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.62.1",
3
+ "version": "2.64.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
+ "packageManager": "npm@10.9.2",
6
7
  "engines": {
7
8
  "node": ">=20"
8
9
  },
package/search-engine.mjs CHANGED
@@ -152,7 +152,7 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
152
152
  * 1. FTS5 MATCH with the sanitized query (AND-by-default), recency-weighted
153
153
  * 2. If AND returns 0 → relaxFtsQueryToOr fallback (mirrors searchObservationsHybrid)
154
154
  *
155
- * Returns the matched observation id, or null. Always skips compressed rows.
155
+ * Always skips compressed rows.
156
156
  *
157
157
  * @param {Database} db
158
158
  * @param {object} opts
@@ -160,7 +160,8 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
160
160
  * @param {string|null} [opts.project] restrict to this project (boost-by-membership; null = no filter)
161
161
  * @param {number} [opts.nowT] Date.now() override (for deterministic tests)
162
162
  * @param {number} [opts.halfLifeMs] recency half-life (default DEFAULT_DECAY_HALF_LIFE_MS)
163
- * @returns {number|null}
163
+ * @returns {{id:number, relaxed:boolean}|null} `relaxed:true` when AND returned 0 and OR rescued —
164
+ * callers should surface a "(relaxed AND→OR)" hint to mirror search transparency.
164
165
  */
165
166
  export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfLifeMs = DEFAULT_DECAY_HALF_LIFE_MS } = {}) {
166
167
  if (!ftsQuery) return null;
@@ -178,13 +179,13 @@ export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfL
178
179
  const stmt = db.prepare(sql);
179
180
  try {
180
181
  const m = stmt.get(ftsQuery, project, project, now);
181
- if (m) return m.id;
182
+ if (m) return { id: m.id, relaxed: false };
182
183
  } catch (e) { debugCatch(e, 'findFtsAnchor-and'); }
183
184
  const orQuery = relaxFtsQueryToOr(ftsQuery);
184
185
  if (orQuery && orQuery !== ftsQuery) {
185
186
  try {
186
187
  const m = stmt.get(orQuery, project, project, now);
187
- if (m) return m.id;
188
+ if (m) return { id: m.id, relaxed: true };
188
189
  } catch (e) { debugCatch(e, 'findFtsAnchor-or'); }
189
190
  }
190
191
  return null;
package/server.mjs CHANGED
@@ -614,11 +614,18 @@ server.registerTool(
614
614
 
615
615
  // Auto-find anchor via FTS (with recency decay). Routes through shared
616
616
  // findFtsAnchor so CLI `timeline --query` and MCP mem_timeline use
617
- // identical AND→OR fallback semantics (paired-path per #8217).
617
+ // identical AND→OR fallback semantics (paired-path per #8217). When the
618
+ // OR fallback fired, surface a hint so the caller knows the match was
619
+ // not an exact AND coverage of the query — mirrors search transparency.
618
620
  if (!anchorId && args.query) {
619
621
  const ftsQuery = sanitizeFtsQuery(args.query);
620
622
  const found = findFtsAnchor(db, { ftsQuery, project: args.project ?? null });
621
- if (found) anchorId = found;
623
+ if (found) {
624
+ anchorId = found.id;
625
+ if (found.relaxed && !anchorNote) {
626
+ anchorNote = `(query "${args.query}" relaxed AND→OR — no row matched all terms)`;
627
+ }
628
+ }
622
629
  }
623
630
 
624
631
  // No anchor: return most recent