claude-mem-lite 2.62.1 → 2.63.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.63.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.63.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/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);
@@ -1593,34 +1612,19 @@ function syncVersions() {
1593
1612
  const version = pkg.version;
1594
1613
  log(`package.json version: ${version}`);
1595
1614
 
1596
- // Sync plugin.json
1597
1615
  const pluginJsonPath = join(PROJECT_DIR, '.claude-plugin', 'plugin.json');
1598
1616
  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
- }
1617
+ const r = bumpJsonField(pluginJsonPath, ['version'], version);
1618
+ ok(r.changed ? `plugin.json: ${r.prev} → ${version}` : `plugin.json: already ${version}`);
1607
1619
  } else {
1608
1620
  warn('plugin.json not found');
1609
1621
  }
1610
1622
 
1611
- // Sync marketplace.json
1612
1623
  const marketJsonPath = join(PROJECT_DIR, '.claude-plugin', 'marketplace.json');
1613
1624
  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
- }
1625
+ const r = bumpJsonField(marketJsonPath, ['plugins', 0, 'version'], version);
1626
+ if (r.prev === undefined) warn('marketplace.json: plugins[0] not found');
1627
+ else ok(r.changed ? `marketplace.json: ${r.prev} → ${version}` : `marketplace.json: already ${version}`);
1624
1628
  } else {
1625
1629
  warn('marketplace.json not found');
1626
1630
  }
@@ -1649,6 +1653,29 @@ function syncVersions() {
1649
1653
  console.log('');
1650
1654
  }
1651
1655
 
1656
+ // Regenerate package-lock.json via npm@10.9.2 to guarantee CI parity. The
1657
+ // drift this prevents: `npm install --package-lock-only` on npm@11+ silently
1658
+ // strips top-level `@emnapi/core` + `@emnapi/runtime` entries when those are
1659
+ // transitive deps of platform-optional bindings (e.g. `@oxc-parser/binding-*`
1660
+ // from knip), and CI's bundled npm@10 (Node 22 default in GitHub Actions)
1661
+ // then refuses `npm ci` with EUSAGE. Same recipe bit twice (#8271 / 2.58.2 /
1662
+ // 2.62.1) before this guard. The packageManager field in package.json
1663
+ // declares the same version for corepack-aware tooling. Network cost: ~5-30s
1664
+ // per release; release cadence makes this acceptable.
1665
+ function regenerateLockfile() {
1666
+ console.log('\nclaude-mem-lite release — regenerate lockfile (npm@10.9.2)\n');
1667
+ try {
1668
+ execFileSync('npx', ['--yes', 'npm@10.9.2', 'install'], {
1669
+ stdio: 'inherit',
1670
+ cwd: PROJECT_DIR,
1671
+ });
1672
+ ok('lockfile regenerated');
1673
+ } catch (e) {
1674
+ fail('lockfile regen failed: ' + e.message);
1675
+ throw e;
1676
+ }
1677
+ }
1678
+
1652
1679
  // ─── Main ───────────────────────────────────────────────────────────────────
1653
1680
 
1654
1681
  export async function main(argv = process.argv.slice(2)) {
@@ -1680,6 +1707,7 @@ export async function main(argv = process.argv.slice(2)) {
1680
1707
  break;
1681
1708
  case 'release':
1682
1709
  syncVersions();
1710
+ if (!flags.has('--no-lock')) regenerateLockfile();
1683
1711
  break;
1684
1712
  default:
1685
1713
  if (IS_NPX) {
@@ -1699,7 +1727,7 @@ Usage:
1699
1727
  node install.mjs cleanup Remove stale temp/staging files
1700
1728
  node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
1701
1729
  node install.mjs self-update Check for and install updates
1702
- node install.mjs release Sync version to plugin.json + marketplace.json
1730
+ 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
1731
 
1704
1732
  npx claude-mem-lite Install via npx (one-liner)
1705
1733
  `);
package/mem-cli.mjs CHANGED
@@ -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'));
@@ -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)
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.62.1",
3
+ "version": "2.63.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