claude-mem-lite 3.96.0 → 3.97.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.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.96.0",
13
+ "version": "3.97.0",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.96.0",
3
+ "version": "3.97.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/adopt-cli.mjs CHANGED
@@ -17,20 +17,28 @@ import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync
17
17
  import { homedir } from 'os';
18
18
  import { join, isAbsolute } from 'path';
19
19
  import {
20
- memdirPath, removePluginSection, removePluginDoc,
21
- isAdopted as memdirIsAdopted, hasPluginState,
20
+ memdirPath,
21
+ removePluginSection,
22
+ removePluginDoc,
23
+ isAdopted as memdirIsAdopted,
24
+ hasPluginState,
22
25
  } from './memdir.mjs';
23
26
  import {
24
- writeManaged, removeManaged, isAdopted as claudeMdIsAdopted,
27
+ writeManaged,
28
+ removeManaged,
29
+ isAdopted as claudeMdIsAdopted,
25
30
  hasResidue as claudeMdHasResidue,
26
- needsRefresh, migrateLegacyMemoryDir, hasLegacyMemdirSentinel,
27
- claudeMdPath, detailDocPath,
31
+ needsRefresh,
32
+ migrateLegacyMemoryDir,
33
+ hasLegacyMemdirSentinel,
34
+ claudeMdPath,
35
+ detailDocPath,
28
36
  } from './claudemd.mjs';
29
- import {
30
- PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc,
31
- } from './adopt-content.mjs';
37
+ import { PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc } from './adopt-content.mjs';
32
38
 
33
- function log(msg) { console.log(msg); }
39
+ function log(msg) {
40
+ console.log(msg);
41
+ }
34
42
 
35
43
  function detectCwd() {
36
44
  return process.env.CLAUDE_PROJECT_DIR || process.env.PWD || process.cwd();
@@ -50,12 +58,16 @@ function listAllMemdirs() {
50
58
  if (existsSync(memdir) && statSync(memdir).isDirectory()) {
51
59
  out.push({ projectSlug: name, memdir });
52
60
  }
53
- } catch { /* ignore entries we can't stat */ }
61
+ } catch {
62
+ /* ignore entries we can't stat */
63
+ }
54
64
  }
55
65
  return out;
56
66
  }
57
67
 
58
- function claudeConfigPath() { return join(homedir(), '.claude.json'); }
68
+ function claudeConfigPath() {
69
+ return join(homedir(), '.claude.json');
70
+ }
59
71
 
60
72
  // Real adopted-project paths come from Claude Code's own ~/.claude.json `projects`
61
73
  // map (keys are absolute cwds Claude Code has opened). The memdir slug under
@@ -71,10 +83,14 @@ function listKnownProjectDirs() {
71
83
  const cfg = JSON.parse(readFileSync(p, 'utf8'));
72
84
  const projects = cfg && cfg.projects && typeof cfg.projects === 'object' ? Object.keys(cfg.projects) : [];
73
85
  return projects.filter((d) => typeof d === 'string' && isAbsolute(d) && existsSync(d));
74
- } catch { return []; }
86
+ } catch {
87
+ return [];
88
+ }
75
89
  }
76
90
 
77
- function hasFlag(args, flag) { return Array.isArray(args) && args.includes(flag); }
91
+ function hasFlag(args, flag) {
92
+ return Array.isArray(args) && args.includes(flag);
93
+ }
78
94
 
79
95
  // ─── Per-project auto-adopt opt-out sentinel ─────────────────────────────────
80
96
  // `<memdir>/.mem-no-auto-adopt` is the durable, project-scoped escape hatch.
@@ -154,14 +170,22 @@ function migrateAll(args) {
154
170
  const force = hasFlag(args, '--force');
155
171
  const dryRun = hasFlag(args, '--dry-run');
156
172
  const dirs = listAllMemdirs();
157
- if (dirs.length === 0) { log('[adopt --all] no memdirs found'); return; }
173
+ if (dirs.length === 0) {
174
+ log('[adopt --all] no memdirs found');
175
+ return;
176
+ }
158
177
 
159
- let removed = 0, absent = 0, skipped = 0;
178
+ let removed = 0,
179
+ absent = 0,
180
+ skipped = 0;
160
181
  for (const { projectSlug, memdir } of dirs) {
161
182
  if (dryRun) {
162
183
  const has = memdirIsAdopted(memdir, PLUGIN_SLUG);
163
- const action = !has ? 'absent'
164
- : (hasPluginState(memdir, PLUGIN_SLUG) || force) ? 'would-remove' : 'would-skip-foreign';
184
+ const action = !has
185
+ ? 'absent'
186
+ : hasPluginState(memdir, PLUGIN_SLUG) || force
187
+ ? 'would-remove'
188
+ : 'would-skip-foreign';
165
189
  log(`[adopt --all --dry-run] ${projectSlug} → ${action}`);
166
190
  if (action === 'would-remove') removed++;
167
191
  else if (action === 'would-skip-foreign') skipped++;
@@ -169,13 +193,19 @@ function migrateAll(args) {
169
193
  continue;
170
194
  }
171
195
  const r = removePluginSection(memdir, PLUGIN_SLUG, { force });
172
- if (r.action === 'removed') { removePluginDoc(memdir, PLUGIN_SLUG); removed++; }
173
- else if (r.action === 'skipped-foreign') skipped++;
196
+ if (r.action === 'removed') {
197
+ removePluginDoc(memdir, PLUGIN_SLUG);
198
+ removed++;
199
+ } else if (r.action === 'skipped-foreign') skipped++;
174
200
  else absent++;
175
201
  }
176
202
  log('');
177
- log(`[adopt --all] legacy memory-dir cleanup over ${dirs.length} project(s): ${removed} cleaned, ${skipped} skipped-foreign, ${absent} none.`);
178
- log('[adopt --all] CLAUDE.md adoption is per-project it runs automatically on each project\'s next SessionStart.');
203
+ log(
204
+ `[adopt --all] legacy memory-dir cleanup over ${dirs.length} project(s): ${removed} cleaned, ${skipped} skipped-foreign, ${absent} none.`,
205
+ );
206
+ log(
207
+ "[adopt --all] CLAUDE.md adoption is per-project — it runs automatically on each project's next SessionStart.",
208
+ );
179
209
  }
180
210
 
181
211
  /**
@@ -205,15 +235,21 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
205
235
  if (!claudeMdIsAdopted(cwd, PLUGIN_SLUG)) {
206
236
  writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
207
237
  action = 'adopted';
208
- } else if (process.env.CLAUDE_MEM_NO_TEMPLATE_REFRESH !== '1'
209
- && needsRefresh(cwd, { slug: PLUGIN_SLUG, version, block, doc })) {
238
+ } else if (
239
+ process.env.CLAUDE_MEM_NO_TEMPLATE_REFRESH !== '1' &&
240
+ needsRefresh(cwd, { slug: PLUGIN_SLUG, version, block, doc })
241
+ ) {
210
242
  writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
211
243
  action = 'refreshed';
212
244
  }
213
245
  if (markerDir && markerKey) writeMarker(markerDir, markerKey);
214
246
  return { ok: true, action };
215
247
  } catch (e) {
216
- try { if (markerDir && markerKey) writeMarker(markerDir, markerKey); } catch { /* best-effort */ }
248
+ try {
249
+ if (markerDir && markerKey) writeMarker(markerDir, markerKey);
250
+ } catch {
251
+ /* best-effort */
252
+ }
217
253
  return { ok: false, action: 'skipped', reason: 'error', err: e };
218
254
  }
219
255
  }
@@ -235,13 +271,15 @@ export function hasAutoAdoptMarker(markerDir, markerKey) {
235
271
  */
236
272
  function cmdDisable(args) {
237
273
  const all = hasFlag(args, '--all');
238
- const targets = all
239
- ? listAllMemdirs().map((m) => m.memdir)
240
- : [memdirPath(detectCwd())];
274
+ const targets = all ? listAllMemdirs().map((m) => m.memdir) : [memdirPath(detectCwd())];
241
275
 
242
- if (targets.length === 0) { log('[adopt --disable] no memdirs found'); return; }
276
+ if (targets.length === 0) {
277
+ log('[adopt --disable] no memdirs found');
278
+ return;
279
+ }
243
280
 
244
- let disabled = 0, already = 0;
281
+ let disabled = 0,
282
+ already = 0;
245
283
  for (const memdir of targets) {
246
284
  if (!existsSync(memdir)) mkdirSync(memdir, { recursive: true });
247
285
  const path = disableSentinelPath(memdir);
@@ -255,7 +293,9 @@ function cmdDisable(args) {
255
293
  disabled++;
256
294
  }
257
295
  log('');
258
- log(`[adopt --disable] ${targets.length} target(s): ${disabled} newly disabled, ${already} already disabled`);
296
+ log(
297
+ `[adopt --disable] ${targets.length} target(s): ${disabled} newly disabled, ${already} already disabled`,
298
+ );
259
299
  }
260
300
 
261
301
  /**
@@ -264,13 +304,15 @@ function cmdDisable(args) {
264
304
  */
265
305
  function cmdEnable(args) {
266
306
  const all = hasFlag(args, '--all');
267
- const targets = all
268
- ? listAllMemdirs().map((m) => m.memdir)
269
- : [memdirPath(detectCwd())];
307
+ const targets = all ? listAllMemdirs().map((m) => m.memdir) : [memdirPath(detectCwd())];
270
308
 
271
- if (targets.length === 0) { log('[adopt --enable] no memdirs found'); return; }
309
+ if (targets.length === 0) {
310
+ log('[adopt --enable] no memdirs found');
311
+ return;
312
+ }
272
313
 
273
- let enabled = 0, absent = 0;
314
+ let enabled = 0,
315
+ absent = 0;
274
316
  for (const memdir of targets) {
275
317
  const path = disableSentinelPath(memdir);
276
318
  if (!existsSync(path)) {
@@ -278,7 +320,11 @@ function cmdEnable(args) {
278
320
  absent++;
279
321
  continue;
280
322
  }
281
- try { unlinkSync(path); } catch { /* best-effort */ }
323
+ try {
324
+ unlinkSync(path);
325
+ } catch {
326
+ /* best-effort */
327
+ }
282
328
  log(`[adopt --enable] ${memdir} → enabled`);
283
329
  enabled++;
284
330
  }
@@ -301,26 +347,35 @@ function statusAll() {
301
347
  }
302
348
 
303
349
  const dirs = listAllMemdirs();
304
- let legacy = 0, disabled = 0;
350
+ let legacy = 0,
351
+ disabled = 0;
305
352
  for (const { memdir } of dirs) {
306
353
  if (memdirIsAdopted(memdir, PLUGIN_SLUG)) legacy++;
307
354
  if (isAutoAdoptDisabled(memdir)) disabled++;
308
355
  }
309
356
  log('');
310
- log(`[adopt --status] scanned ${dirs.length} memdir(s): ${legacy} with legacy sentinel (await migration), ${disabled} auto-adopt-disabled.`);
311
- if (legacy > 0) log('[adopt --status] run `claude-mem-lite adopt --all` to sweep legacy memory-dir sentinels now.');
357
+ log(
358
+ `[adopt --status] scanned ${dirs.length} memdir(s): ${legacy} with legacy sentinel (await migration), ${disabled} auto-adopt-disabled.`,
359
+ );
360
+ if (legacy > 0)
361
+ log('[adopt --status] run `claude-mem-lite adopt --all` to sweep legacy memory-dir sentinels now.');
312
362
 
313
363
  const known = listKnownProjectDirs();
314
364
  let adoptedCount = 0;
315
365
  for (const dir of known) if (claudeMdHasResidue(dir, PLUGIN_SLUG)) adoptedCount++;
316
- log(`[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block or partial residue (detail doc/state).`);
317
- if (adoptedCount > 0) log('[adopt --status] run `claude-mem-lite unadopt --all` to remove every CLAUDE.md block.');
366
+ log(
367
+ `[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block or partial residue (detail doc/state).`,
368
+ );
369
+ if (adoptedCount > 0)
370
+ log('[adopt --status] run `claude-mem-lite unadopt --all` to remove every CLAUDE.md block.');
318
371
 
319
372
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
320
373
  const noAutoAdopt = process.env.MEM_NO_AUTO_ADOPT === '1' ? '1 (opt-out)' : 'unset';
321
374
  log('');
322
375
  log('Auto-adopt gates (next SessionStart fires only if these pass):');
323
- log(` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (any install path is consent; gate is the per-project opt-out below)`);
376
+ log(
377
+ ` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (any install path is consent; gate is the per-project opt-out below)`,
378
+ );
324
379
  log(` MEM_NO_AUTO_ADOPT = ${noAutoAdopt} (global escape hatch)`);
325
380
  log('Per-project opt-out: `claude-mem-lite adopt --disable` (run --enable to re-arm).');
326
381
  }
@@ -348,20 +403,28 @@ function unadoptAll(args) {
348
403
 
349
404
  // 1. New scheme: scrub CLAUDE.md managed blocks across known project paths.
350
405
  const projectDirs = listKnownProjectDirs();
351
- let blocks = 0, partial = 0;
406
+ let blocks = 0,
407
+ partial = 0;
352
408
  for (const dir of projectDirs) {
353
409
  // hasResidue, not isAdopted: the sweep must also catch PARTIAL residue
354
410
  // (block without detail doc, or an orphaned doc/state sidecar) —
355
411
  // isAdopted's block-AND-doc gate skipped those projects forever.
356
412
  if (!claudeMdHasResidue(dir, PLUGIN_SLUG)) continue;
357
413
  if (dryRun) {
358
- log(`[unadopt --all --dry-run] ${dir} → would-remove plugin residue (CLAUDE.md block and/or detail doc/state)`);
414
+ log(
415
+ `[unadopt --all --dry-run] ${dir} → would-remove plugin residue (CLAUDE.md block and/or detail doc/state)`,
416
+ );
359
417
  blocks++;
360
418
  continue;
361
419
  }
362
420
  const r = removeManaged(dir, PLUGIN_SLUG);
363
- if (r.action === 'removed') { log(`[unadopt --all] ${dir} → removed`); blocks++; }
364
- else { log(`[unadopt --all] ${dir} → cleaned partial residue (detail doc/state, no block)`); partial++; }
421
+ if (r.action === 'removed') {
422
+ log(`[unadopt --all] ${dir} → removed`);
423
+ blocks++;
424
+ } else {
425
+ log(`[unadopt --all] ${dir} → cleaned partial residue (detail doc/state, no block)`);
426
+ partial++;
427
+ }
365
428
  }
366
429
 
367
430
  // 2. Legacy memory-dir cleanup across every memdir (foreign-content guarded).
@@ -373,14 +436,21 @@ function unadoptAll(args) {
373
436
  continue;
374
437
  }
375
438
  const r = removePluginSection(memdir, PLUGIN_SLUG, { force });
376
- if (r.action === 'removed') { removePluginDoc(memdir, PLUGIN_SLUG); legacy++; }
439
+ if (r.action === 'removed') {
440
+ removePluginDoc(memdir, PLUGIN_SLUG);
441
+ legacy++;
442
+ }
377
443
  }
378
444
 
379
445
  log('');
380
446
  const partialNote = partial > 0 ? ` (+${partial} partial-residue cleanup(s))` : '';
381
- log(`[unadopt --all] ${dryRun ? 'would remove' : 'removed'} ${blocks} CLAUDE.md block(s)${partialNote} across ${projectDirs.length} known project(s); ${legacy} legacy memory-dir sentinel(s) ${dryRun ? 'pending' : 'cleaned'}.`);
447
+ log(
448
+ `[unadopt --all] ${dryRun ? 'would remove' : 'removed'} ${blocks} CLAUDE.md block(s)${partialNote} across ${projectDirs.length} known project(s); ${legacy} legacy memory-dir sentinel(s) ${dryRun ? 'pending' : 'cleaned'}.`,
449
+ );
382
450
  if (projectDirs.length === 0) {
383
- log('[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run `claude-mem-lite unadopt` from inside it.');
451
+ log(
452
+ '[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run `claude-mem-lite unadopt` from inside it.',
453
+ );
384
454
  }
385
455
  }
386
456
 
@@ -395,8 +465,12 @@ export function cmdUnadopt(args = []) {
395
465
 
396
466
  const cwd = detectCwd();
397
467
  if (dryRun) {
398
- const blockState = claudeMdHasResidue(cwd, PLUGIN_SLUG) ? 'would-remove CLAUDE.md block + detail doc' : 'no CLAUDE.md block';
399
- const legacy = hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG) ? 'would-clean legacy memory-dir sentinel' : 'no legacy residue';
468
+ const blockState = claudeMdHasResidue(cwd, PLUGIN_SLUG)
469
+ ? 'would-remove CLAUDE.md block + detail doc'
470
+ : 'no CLAUDE.md block';
471
+ const legacy = hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG)
472
+ ? 'would-clean legacy memory-dir sentinel'
473
+ : 'no legacy residue';
400
474
  log(`[unadopt --dry-run] ${cwd}`);
401
475
  log(` ${blockState}`);
402
476
  log(` ${legacy}`);
package/bash-utils.mjs CHANGED
@@ -6,12 +6,38 @@ import { basename } from 'path';
6
6
  // Read/search commands whose output legitimately contains "error"-like keywords without
7
7
  // being a failure. Matched against the PRIMARY command (see isReadOnlyCommand).
8
8
  const SEARCH_VERBS = new Set([
9
- 'grep', 'rg', 'ag', 'ack', 'cat', 'head', 'tail', 'less', 'more', 'find', 'locate', 'wc', 'file', 'which', 'type',
9
+ 'grep',
10
+ 'rg',
11
+ 'ag',
12
+ 'ack',
13
+ 'cat',
14
+ 'head',
15
+ 'tail',
16
+ 'less',
17
+ 'more',
18
+ 'find',
19
+ 'locate',
20
+ 'wc',
21
+ 'file',
22
+ 'which',
23
+ 'type',
10
24
  ]);
11
25
  // Command prefixes that wrap the real command (env-assignments handled separately).
12
26
  const CMD_WRAPPERS = new Set(['sudo', 'doas', 'env', 'time', 'command', 'nice', 'nohup', 'stdbuf', 'xargs']);
13
27
  // git read subcommands whose output contains commit/log/match text, not failures.
14
- const GIT_READ_SUBCMDS = new Set(['grep', 'log', 'show', 'diff', 'blame', 'ls-files', 'cat-file', 'whatchanged', 'shortlog', 'reflog', 'status']);
28
+ const GIT_READ_SUBCMDS = new Set([
29
+ 'grep',
30
+ 'log',
31
+ 'show',
32
+ 'diff',
33
+ 'blame',
34
+ 'ls-files',
35
+ 'cat-file',
36
+ 'whatchanged',
37
+ 'shortlog',
38
+ 'reflog',
39
+ 'status',
40
+ ]);
15
41
 
16
42
  // Hard failure fingerprints — a real crash / thrown exception / non-zero-exit marker,
17
43
  // as opposed to output that merely CONTAINS the word "error" (search results, log
@@ -21,7 +47,8 @@ const GIT_READ_SUBCMDS = new Set(['grep', 'log', 'show', 'diff', 'blame', 'ls-fi
21
47
  // intentionally excluded — they appear too often in benign search/log output. Gates
22
48
  // the bugfix-shape save-nudge (lib/cite-back-hint.mjs) so `node cli.mjs search "error"`
23
49
  // + an edit in the same episode no longer looks like an unsaved fix.
24
- const HARD_ERROR_RE = /\bERR!|\bpanic\b|traceback|segfault|core dumped|\benoent\b|command not found|assertion\s?error|\n\s+at\s+\S|(?:type|reference|range|syntax|eval|uri)error:/i;
50
+ const HARD_ERROR_RE =
51
+ /\bERR!|\bpanic\b|traceback|segfault|core dumped|\benoent\b|command not found|assertion\s?error|\n\s+at\s+\S|(?:type|reference|range|syntax|eval|uri)error:/i;
25
52
 
26
53
  // True when the command's PRIMARY operation (left of the first pipe, past any
27
54
  // env-assignments / wrapper like `sudo`/`env`/`time`) is a read/search — including
@@ -66,9 +93,12 @@ export function detectBashSignificance(input, response) {
66
93
  // stays a real failure while `sudo grep`, `git grep`, `git log --grep`, `cat f | head`
67
94
  // remain exempt and `run-cat-tests` doesn't trip a substring match.
68
95
  const isSearchCmd = isReadOnlyCommand(cmd);
69
- const looksLikeError = !isSearchCmd
70
- && /\berror\b|\bERR!|fail(ed|ure)?|exception|panic|traceback|errno|enoent|command not found/i.test(response)
71
- && response.length > 15;
96
+ const looksLikeError =
97
+ !isSearchCmd &&
98
+ /\berror\b|\bERR!|fail(ed|ure)?|exception|panic|traceback|errno|enoent|command not found/i.test(
99
+ response,
100
+ ) &&
101
+ response.length > 15;
72
102
  // Green test summary exemption — "0 fail/failed/failures" in test-runner
73
103
  // output (bun/jest/pytest) gets matched by the broad `fail(ed|ure)?` token
74
104
  // above, driving episode.isError=true for passing runs. A live cluster-merge
@@ -76,45 +106,79 @@ export function detectBashSignificance(input, response) {
76
106
  // from this path. Flip back to non-error iff a "0 fail" marker is present
77
107
  // AND no hard-error signal (panic / ENOENT / AssertionError / TypeError /
78
108
  // explicit FAIL banner / npm ERR!) coexists in the output.
79
- const hasGreenTestSummary = looksLikeError
80
- && /\b0\s+(fail|failed|failures)\b/i.test(response);
109
+ const hasGreenTestSummary = looksLikeError && /\b0\s+(fail|failed|failures)\b/i.test(response);
81
110
  // NOTE: do not add `\bFAIL\s` here — with /i flag it would re-match the
82
111
  // very `0 fail\n` token green-summary is trying to exempt. A real test
83
112
  // failure produces "N fail" (N≥1) which never triggers hasGreenTestSummary,
84
113
  // so a uppercase-FAIL fingerprint isn't needed for correctness.
85
- const hasHardErrorSignal = hasGreenTestSummary
86
- && /\bERR!|panic|traceback|enoent|command not found|exception|AssertionError|TypeError:|SyntaxError:/i.test(response);
114
+ const hasHardErrorSignal =
115
+ hasGreenTestSummary &&
116
+ /\bERR!|panic|traceback|enoent|command not found|exception|AssertionError|TypeError:|SyntaxError:/i.test(
117
+ response,
118
+ );
87
119
  const isError = looksLikeError && !(hasGreenTestSummary && !hasHardErrorSignal);
88
120
  // Strict subset of isError: a genuine failure fingerprint, not just the word "error"
89
121
  // in benign output. Consumers that must avoid false positives (the bugfix-shape
90
122
  // save-nudge) gate on this instead of isError.
91
123
  const isHardError = isError && HARD_ERROR_RE.test(response);
92
124
  // Match actual test runner invocations, not commands that merely reference "test" as a keyword
93
- const isTest = /\b(npm\s+test|npm\s+run\s+test|yarn\s+test|pnpm\s+test|pnpm\s+run\s+test|bun\s+test|go\s+test|cargo\s+test)\b/i.test(cmd)
94
- || /\b(jest|pytest|vitest|mocha|cypress|playwright)\b/i.test(cmd);
125
+ const isTest =
126
+ /\b(npm\s+test|npm\s+run\s+test|yarn\s+test|pnpm\s+test|pnpm\s+run\s+test|bun\s+test|go\s+test|cargo\s+test)\b/i.test(
127
+ cmd,
128
+ ) || /\b(jest|pytest|vitest|mocha|cypress|playwright)\b/i.test(cmd);
95
129
  const isBuild = /\b(build|compile|tsc|webpack|vite|rollup|esbuild|make|cargo)\b/i.test(cmd);
96
130
  // Allow intervening global git options (`-C <path>`, `-c k=v`, `--no-pager`, …) between
97
131
  // `git` and the subcommand — `git -C /repo push` is the standard multi-repo/scripted form.
98
- const isGit = /\bgit\s+(?:(?:-[cC]\s+\S+|--?[\w-]+(?:=\S+)?)\s+)*(commit|merge|rebase|cherry-pick|push)\b/i.test(cmd);
132
+ const isGit =
133
+ /\bgit\s+(?:(?:-[cC]\s+\S+|--?[\w-]+(?:=\S+)?)\s+)*(commit|merge|rebase|cherry-pick|push)\b/i.test(cmd);
99
134
  // Deploy + publish/release: the actual "ship". Package publish and GitHub
100
135
  // release are rare, high-value events; without them a release session records
101
136
  // the git push but not the publish that defines it. `npm run publish-*` is
102
137
  // excluded (custom script, ambiguous) — only the direct publish verb counts.
103
- const isDeploy = /\b(deploy|docker|kubectl|terraform)\b/i.test(cmd)
104
- || /\b(?:npm|pnpm|yarn|bun|cargo)\s+publish\b/i.test(cmd)
105
- || /\bgh\s+release\s+(?:create|edit|upload|delete)\b/i.test(cmd)
106
- || /\btwine\s+upload\b/i.test(cmd);
138
+ const isDeploy =
139
+ /\b(deploy|docker|kubectl|terraform)\b/i.test(cmd) ||
140
+ /\b(?:npm|pnpm|yarn|bun|cargo)\s+publish\b/i.test(cmd) ||
141
+ /\bgh\s+release\s+(?:create|edit|upload|delete)\b/i.test(cmd) ||
142
+ /\btwine\s+upload\b/i.test(cmd);
107
143
  return {
108
- isError, isHardError, isTest, isBuild, isGit, isDeploy,
144
+ isError,
145
+ isHardError,
146
+ isTest,
147
+ isBuild,
148
+ isGit,
149
+ isDeploy,
109
150
  isSignificant: isError || isTest || isBuild || isGit || isDeploy,
110
151
  };
111
152
  }
112
153
 
113
154
  const ERROR_STOP_WORDS = new Set([
114
- 'error', 'failed', 'cannot', 'could', 'with', 'from', 'that', 'this',
115
- 'have', 'been', 'were', 'does', 'will', 'would', 'should', 'must',
116
- 'true', 'false', 'null', 'undefined', 'function', 'return', 'const',
117
- 'node', 'require', 'stack', 'trace',
155
+ 'error',
156
+ 'failed',
157
+ 'cannot',
158
+ 'could',
159
+ 'with',
160
+ 'from',
161
+ 'that',
162
+ 'this',
163
+ 'have',
164
+ 'been',
165
+ 'were',
166
+ 'does',
167
+ 'will',
168
+ 'would',
169
+ 'should',
170
+ 'must',
171
+ 'true',
172
+ 'false',
173
+ 'null',
174
+ 'undefined',
175
+ 'function',
176
+ 'return',
177
+ 'const',
178
+ 'node',
179
+ 'require',
180
+ 'stack',
181
+ 'trace',
118
182
  ]);
119
183
 
120
184
  const ERROR_LINE_RE = /error|fail|exception|cannot|not found|undefined|null/i;
@@ -183,10 +247,15 @@ const ERROR_NAMER_MAX = 1;
183
247
  function collectErrorTerms(cmd, response) {
184
248
  const seen = new Set();
185
249
  const cmdWords = [];
186
- const cmdParts = String(cmd || '').split(/[\s/\\|&;]+/).filter(w => w.length > 2 && !/^-/.test(w));
250
+ const cmdParts = String(cmd || '')
251
+ .split(/[\s/\\|&;]+/)
252
+ .filter((w) => w.length > 2 && !/^-/.test(w));
187
253
  for (const w of cmdParts.slice(0, 3)) {
188
254
  const lw = w.toLowerCase();
189
- if (!ERROR_STOP_WORDS.has(lw) && !seen.has(lw)) { seen.add(lw); cmdWords.push(lw); }
255
+ if (!ERROR_STOP_WORDS.has(lw) && !seen.has(lw)) {
256
+ seen.add(lw);
257
+ cmdWords.push(lw);
258
+ }
190
259
  }
191
260
  const errWords = [];
192
261
  // The failure's NAME goes in first — see ERROR_NAMER_RE for the measurement that put
@@ -218,11 +287,16 @@ function collectErrorTerms(cmd, response) {
218
287
  .filter((l) => ERROR_LINE_RE.test(l) || HARD_ERROR_RE.test(l))
219
288
  .slice(0, 3);
220
289
  for (const line of errLines) {
221
- const tokens = line.replace(/[^a-zA-Z0-9_.-]/g, ' ').split(/\s+/)
222
- .filter(w => w.length > 3 && !/^\d+$/.test(w));
290
+ const tokens = line
291
+ .replace(/[^a-zA-Z0-9_.-]/g, ' ')
292
+ .split(/\s+/)
293
+ .filter((w) => w.length > 3 && !/^\d+$/.test(w));
223
294
  for (const t of tokens.slice(0, 5)) {
224
295
  const lt = t.toLowerCase();
225
- if (!ERROR_STOP_WORDS.has(lt) && !seen.has(lt)) { seen.add(lt); errWords.push(lt); }
296
+ if (!ERROR_STOP_WORDS.has(lt) && !seen.has(lt)) {
297
+ seen.add(lt);
298
+ errWords.push(lt);
299
+ }
226
300
  }
227
301
  }
228
302
  return { cmdWords, errWords };
@@ -376,9 +450,11 @@ export function extractFilePaths(input) {
376
450
  if (match) {
377
451
  for (const m of match) {
378
452
  const p = m.trim();
379
- if (!isExcludedPath(p)
453
+ if (
454
+ !isExcludedPath(p) &&
380
455
  // Skip single-component paths like /exit, /clear — likely slash commands, not files
381
- && (p.indexOf('/', 1) !== -1 || /\.\w+$/.test(p))) {
456
+ (p.indexOf('/', 1) !== -1 || /\.\w+$/.test(p))
457
+ ) {
382
458
  paths.push(p);
383
459
  }
384
460
  }