claude-mem-lite 2.47.0 → 2.48.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.47.0",
13
+ "version": "2.48.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.47.0",
3
+ "version": "2.48.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/hook-context.mjs CHANGED
@@ -2,13 +2,13 @@
2
2
  // Handles adaptive time windows, token-budgeted selection, and legacy CLAUDE.md cleanup.
3
3
 
4
4
  import { basename, join } from 'path';
5
- import { readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
5
+ import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
6
6
  import {
7
- estimateTokens, truncate, typeIcon, fmtTime,
7
+ estimateTokens, truncate, typeIcon, fmtTime, inferProject,
8
8
  debugLog, debugCatch,
9
9
  DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause,
10
10
  } from './utils.mjs';
11
- import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, effectiveQuiet } from './hook-shared.mjs';
11
+ import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet } from './hook-shared.mjs';
12
12
  import { extractUnfinishedSummary } from './hook-handoff.mjs';
13
13
 
14
14
  /**
@@ -176,9 +176,26 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
176
176
  * at the seam. Uses atomic tmp+rename write.
177
177
  */
178
178
  export function cleanupClaudeMdLegacyBlock() {
179
+ // v2.48 P2-4: idempotent marker. First run (whether it finds a block or not,
180
+ // whether CLAUDE.md exists or not) drops a project-scoped marker in RUNTIME_DIR.
181
+ // Subsequent SessionStarts short-circuit here — no CLAUDE.md stat, no regex scan.
182
+ // Recovery path if a user manually re-adds a legacy block: delete the marker
183
+ // file (`~/.claude-mem-lite/runtime/.legacy-claude-md-cleaned-<project>`) and
184
+ // the next SessionStart will sweep again.
185
+ const markerPath = join(RUNTIME_DIR, `.legacy-claude-md-cleaned-${inferProject()}`);
186
+ if (existsSync(markerPath)) return;
187
+
179
188
  const claudeMdPath = join(inferProjectDir(), 'CLAUDE.md');
180
189
  let content;
181
- try { content = readFileSync(claudeMdPath, 'utf8'); } catch { return; }
190
+ try { content = readFileSync(claudeMdPath, 'utf8'); } catch {
191
+ // CLAUDE.md missing — still drop the marker so we don't re-stat every session
192
+ try { writeFileSync(markerPath, String(Date.now())); } catch {}
193
+ return;
194
+ }
195
+
196
+ // Helper: drop the marker regardless of exit path (found / not found / write failed).
197
+ // Kept inline so the early-return sites below stay readable.
198
+ const dropMarker = () => { try { writeFileSync(markerPath, String(Date.now())); } catch {} };
182
199
 
183
200
  const startTag = '<claude-mem-context>';
184
201
  const endTag = '</claude-mem-context>';
@@ -187,7 +204,10 @@ export function cleanupClaudeMdLegacyBlock() {
187
204
  // (e.g. inside a code block in architecture notes) are not accidentally swept.
188
205
  const startIdx = content.lastIndexOf(startTag);
189
206
  const endIdx = content.lastIndexOf(endTag);
190
- if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) return;
207
+ if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) {
208
+ dropMarker();
209
+ return;
210
+ }
191
211
 
192
212
  // Extend forward to swallow a trailing newline so we don't leave a stranded blank line.
193
213
  let removeEnd = endIdx + endTag.length;
@@ -213,15 +233,17 @@ export function cleanupClaudeMdLegacyBlock() {
213
233
  // Collapse any ≥3 consecutive newlines to two, then ensure exactly one trailing newline.
214
234
  const normalized = cleaned.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n');
215
235
 
216
- if (normalized === content) return;
236
+ if (normalized === content) { dropMarker(); return; }
217
237
 
218
238
  const tmp = claudeMdPath + '.mem-tmp';
219
239
  try {
220
240
  writeFileSync(tmp, normalized);
221
241
  renameSync(tmp, claudeMdPath);
242
+ dropMarker();
222
243
  } catch (e) {
223
244
  try { unlinkSync(tmp); } catch {}
224
245
  debugLog('ERROR', 'cleanupClaudeMdLegacyBlock', `CLAUDE.md write failed: ${e.message}`);
246
+ // Intentionally do NOT drop the marker on write failure — retry next session.
225
247
  }
226
248
  }
227
249
 
package/install.mjs CHANGED
@@ -289,6 +289,17 @@ async function install() {
289
289
  const manifestSrc = join(PROJECT_DIR, 'registry', 'preinstalled.json');
290
290
  if (existsSync(manifestSrc)) copyFileSync(manifestSrc, join(registryDir, 'preinstalled.json'));
291
291
  ok('Source files copied to ~/.claude-mem-lite/');
292
+
293
+ // v2.48 P1-4: prune stale top-level .mjs + 0-byte .db files left behind by
294
+ // prior upgrades (e.g. dispatch.mjs removed in v2.20.0, zero-byte mem.db /
295
+ // memory.db / registry.db from pre-consolidation installs). Subdirs +
296
+ // symlinks + non-empty DBs are always preserved.
297
+ try {
298
+ const pruned = pruneStaleInstallFiles(DATA_DIR, SOURCE_FILES);
299
+ if (pruned.length > 0) {
300
+ ok(`Pruned ${pruned.length} stale file(s): ${pruned.map(p => p.split('/').pop()).join(', ')}`);
301
+ }
302
+ } catch (e) { /* prune is best-effort — never block install */ void e; }
292
303
  }
293
304
 
294
305
  // 2. npm install (skip for --dev: node_modules is symlinked)
@@ -1295,6 +1306,57 @@ function hasMemHooksConfigured(settings) {
1295
1306
  );
1296
1307
  }
1297
1308
 
1309
+ /**
1310
+ * v2.48 P1-4: prune top-level stale files left behind by removed-module upgrades.
1311
+ *
1312
+ * Strict whitelist: only removes files under `dataDir` (no recursion) that match
1313
+ * - `*.mjs` whose basename is NOT in SOURCE_FILES (comparing against both the
1314
+ * bare entry and any `subdir/basename` entry flattened to its basename — the
1315
+ * prune intentionally skips subdir files; see below)
1316
+ * - 0-byte `.db` files that are NOT in the protected-db allow-list
1317
+ *
1318
+ * Protections (never touched):
1319
+ * - subdirectories (managed/, runtime/, scripts/, lib/, cli/, commands/, server/, node_modules/, .claude-plugin/, registry/, etc.)
1320
+ * - non-empty `.db` files — real data risk, always preserved
1321
+ * - WAL/SHM (`*-wal`, `*-shm`) transients
1322
+ * - files not ending in `.mjs` or `.db`
1323
+ * - the two canonical DBs (`claude-mem-lite.db`, `resource-registry.db`) even when 0-byte (fresh-install transient state)
1324
+ *
1325
+ * @param {string} dataDir Absolute path, typically `~/.claude-mem-lite`
1326
+ * @param {string[]} sourceFiles SOURCE_FILES manifest
1327
+ * @returns {string[]} Absolute paths of files that were deleted (ordered by readdir)
1328
+ */
1329
+ export function pruneStaleInstallFiles(dataDir, sourceFiles) {
1330
+ if (!existsSync(dataDir)) return [];
1331
+ // Flatten manifest to just top-level basenames. SOURCE_FILES contains entries
1332
+ // like 'lib/activity.mjs' — those belong to a subdir and prune never touches
1333
+ // subdirs anyway. For top-level entries ('server.mjs'), basename === entry.
1334
+ const topLevelAllowed = new Set(
1335
+ sourceFiles
1336
+ .filter(f => !f.includes('/'))
1337
+ .map(f => f)
1338
+ );
1339
+ const PROTECTED_DBS = new Set(['claude-mem-lite.db', 'resource-registry.db']);
1340
+ const removed = [];
1341
+ let entries;
1342
+ try { entries = readdirSync(dataDir); } catch { return removed; }
1343
+ for (const name of entries) {
1344
+ const full = join(dataDir, name);
1345
+ let st;
1346
+ try { st = lstatSync(full); } catch { continue; }
1347
+ // Skip directories and symlinks (dev mode uses symlinks; treat as intentional).
1348
+ if (!st.isFile()) continue;
1349
+ if (name.endsWith('.mjs') && !topLevelAllowed.has(name)) {
1350
+ try { unlinkSync(full); removed.push(full); } catch { /* best-effort */ }
1351
+ continue;
1352
+ }
1353
+ if (name.endsWith('.db') && !PROTECTED_DBS.has(name) && st.size === 0) {
1354
+ try { unlinkSync(full); removed.push(full); } catch { /* best-effort */ }
1355
+ }
1356
+ }
1357
+ return removed;
1358
+ }
1359
+
1298
1360
  export function clearPluginDisabledMarkerForDirectInstall(settings) {
1299
1361
  if (settings?.enabledPlugins?.[PLUGIN_KEY] !== false) return false;
1300
1362
  delete settings.enabledPlugins[PLUGIN_KEY];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.47.0",
3
+ "version": "2.48.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {