claude-mem-lite 6.29.3 → 6.30.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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.29.3",
12
+ "version": "6.30.0",
13
13
  "source": "./",
14
14
  "homepage": "https://github.com/sdsrss/claude-mem-lite",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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)."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.29.3",
3
+ "version": "6.30.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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
@@ -50,7 +50,7 @@ import {
50
50
  claudeMdPath,
51
51
  detailDocPath,
52
52
  addAgentsImports,
53
- holdsOnlyOwnLines,
53
+ holdsOnlyPluginLines,
54
54
  straySentinelLines,
55
55
  } from './claudemd.mjs';
56
56
  import { PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc } from './adopt-content.mjs';
@@ -77,7 +77,9 @@ import {
77
77
  isSharedAncestor,
78
78
  samePath,
79
79
  localFileLinked,
80
+ userInstructionFiles,
80
81
  } from './lib/local-steering.mjs';
82
+ import { CLI_INVOKE } from './cli-path.mjs';
81
83
 
82
84
  /**
83
85
  * Remove the auto-written local block (CLAUDE.local.md, or the D#212 rules file) for the project at
@@ -226,7 +228,7 @@ function adoptOne(cwd, { force, dryRun }) {
226
228
  const sharedNote = (verb) =>
227
229
  switchesOffBelow
228
230
  ? [
229
- ` ⚠ ${claudeMdPath(cwd)} ${verb} Claude Code reading the AGENTS.md of every project below ${cwd} that has one; to steer one project, run adopt inside it instead${verb === 'stops' ? ' (`claude-mem-lite unadopt` here takes this back)' : ''}, or set Project instructions to claude-md-and-agents-md in /config`,
231
+ ` ⚠ ${claudeMdPath(cwd)} ${verb} Claude Code reading the AGENTS.md of every project below ${cwd} that has one; to steer one project, run adopt inside it instead${verb === 'stops' ? ` (\`${CLI_INVOKE} unadopt\` here takes this back)` : ''}, or set Project instructions to claude-md-and-agents-md in /config`,
230
232
  ]
231
233
  : [];
232
234
 
@@ -262,7 +264,12 @@ function adoptOne(cwd, { force, dryRun }) {
262
264
  : local.action === 'failed'
263
265
  ? ` (could not remove the block from ${local.path}, so sessions here load it twice)`
264
266
  : ` (+removed the block from ${local.path})`;
265
- log(`[adopt] ${cwd} → ${r.action}${migNote}${localNote}${importNote}`);
267
+ // The block's disposition alone read "unchanged" over a rewritten detail doc (D#470).
268
+ const docNote =
269
+ r.action === 'unchanged' && r.docAction !== 'unchanged'
270
+ ? ` (+${r.docAction === 'created' ? 'wrote' : 'refreshed'} detail doc)`
271
+ : '';
272
+ log(`[adopt] ${cwd} → ${r.action}${docNote}${migNote}${localNote}${importNote}`);
266
273
  for (const line of elsewhere('stops')) log(line);
267
274
  for (const line of sharedNote('stops')) log(line);
268
275
  // The copy that could not come out loads beside CLAUDE.md: said, and not a success (R3-4).
@@ -373,13 +380,17 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
373
380
  // cost most of the proactive memory writes (1.5 vs 5.25 per trajectory) and never reached
374
381
  // subagents (0/12). Inside a git work tree the block now goes to CLAUDE.local.md, which the
375
382
  // host loads like CLAUDE.md and info/exclude keeps out of commits (5.25 writes, 12/12).
376
- const hasBlock = blockIn(cwd);
383
+ // managed-only loads no project CLAUDE.md either (code.claude.com/docs/en/memory): a block in
384
+ // one steers nothing, so the session goes on to the refused local file and injects, and the
385
+ // file stays as it is (v6.30.0 pre-tag defects review P2-2).
386
+ const managedOnly = userInstructionFiles() === 'managed-only';
387
+ const hasBlock = !managedOnly && blockIn(cwd);
377
388
  if (!hasBlock) {
378
389
  if (markerDir && markerKey) writeMarker(markerDir, markerKey);
379
390
  // A damaged sentinel line (`… --> (team note)`) leaves the block's text in CLAUDE.md, which
380
391
  // the host loads, while the block no longer parses: a copy written to CLAUDE.local.md or
381
392
  // injected here was a second one. Neither; `adopt` names the line and the fix (D#369 B14).
382
- const stray = straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
393
+ const stray = managedOnly ? [] : straySentinelLines(claudeMdPath(cwd), PLUGIN_SLUG);
383
394
  if (stray.length > 0) return { ok: true, action: 'damaged', reason: 'stray-sentinel', lines: stray };
384
395
  const root = localSteeringRoot(cwd);
385
396
  if (root) {
@@ -387,7 +398,13 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
387
398
  // loads the root's CLAUDE.md as an ancestor, so a block there already steers this
388
399
  // session; and an opt-out recorded for the root covers the file that lives there.
389
400
  if (!samePath(root, cwd)) {
390
- if (blockIn(root)) return { ok: true, action: 'already-adopted', reason: 'root-claude-md' };
401
+ const above = managedOnly ? null : blockAbove(cwd, root);
402
+ if (above)
403
+ return {
404
+ ok: true,
405
+ action: 'already-adopted',
406
+ reason: samePath(above, root) ? 'root-claude-md' : 'ancestor-claude-md',
407
+ };
391
408
  // Off for the project means off here too: no file, no injected copy, no /adopt offer
392
409
  // (delta review P2-2; lib/quiet-scope.mjs mirrors it).
393
410
  if (isAutoAdoptDisabledFor(root)) return { ok: true, action: 'disabled', reason: 'root-disabled' };
@@ -421,7 +438,11 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
421
438
  agentsMd: r.agentsMd,
422
439
  };
423
440
  }
424
- return { ok: true, action: 'inject' };
441
+ // managed-only loads no project CLAUDE.md, so the hook does not offer /adopt (v6.30.0 pre-tag
442
+ // claims review P2-5; inside a git work tree the reason is local-managed-only).
443
+ return managedOnly
444
+ ? { ok: true, action: 'inject', reason: 'managed-only' }
445
+ : { ok: true, action: 'inject' };
425
446
  }
426
447
  dropLocalSteering(cwd, { atRootOnly: true, keepTracked: true });
427
448
  let action = 'already-adopted';
@@ -462,8 +483,8 @@ function syncAgentsImports(cwd) {
462
483
  // project below it, each asking to approve an import from outside its directory.
463
484
  if (isSharedAncestor(cwd)) return null;
464
485
  // The cheap check first: most CLAUDE.md files hold the user's own lines, and the full answer
465
- // asks git.
466
- if (!holdsOnlyOwnLines(claudeMdPath(cwd), PLUGIN_SLUG)) return null;
486
+ // asks git. Another plugin's managed block counts as plugin text, not the user's (D#220).
487
+ if (!holdsOnlyPluginLines(claudeMdPath(cwd), PLUGIN_SLUG)) return null;
467
488
  const agents = agentsMdForNewClaudeMd(cwd, PLUGIN_SLUG);
468
489
  if (!agents) return null;
469
490
  const imported =
@@ -501,6 +522,16 @@ function blockIn(dir) {
501
522
  }
502
523
  }
503
524
 
525
+ // The nearest directory above `cwd`, up to and including `root`, whose CLAUDE.md carries the block:
526
+ // the host loads it as an ancestor. `adopt` run in packages/ and a session in packages/web/src got a
527
+ // root CLAUDE.local.md on top of packages/CLAUDE.md, the block twice (D#177(2)).
528
+ function blockAbove(cwd, root) {
529
+ for (let d = dirname(cwd); ; d = dirname(d)) {
530
+ if (blockIn(d)) return d;
531
+ if (samePath(d, root) || dirname(d) === d) return null;
532
+ }
533
+ }
534
+
504
535
  function writeMarker(markerDir, markerKey) {
505
536
  if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
506
537
  const path = join(markerDir, `.auto-adopt-${markerKey}`);
@@ -620,9 +651,11 @@ function cmdEnable(args) {
620
651
  function localSteeringStatus(cwd) {
621
652
  const root = localSteeringRoot(cwd);
622
653
  const runs = autoAdoptRuns(cwd, root);
623
- // CLAUDE.md first: where it carries the block, no local file is written and nothing is injected,
624
- // in a git work tree or not (pre-tag delta review of the D#212 repairs, D11).
625
- if (blockIn(cwd) || (root && blockIn(root))) {
654
+ // CLAUDE.md first: where it carries the block, no local file is written, in a git work tree or not
655
+ // (pre-tag delta review of the D#212 repairs, D11).
656
+ if (blockIn(cwd) || (root && !samePath(root, cwd) && blockAbove(cwd, root))) {
657
+ if (userInstructionFiles() === 'managed-only')
658
+ return '⚠ CLAUDE.md carries the block, but Project instructions is set to managed-only, which does not load it';
626
659
  const cur = root ? readLocalSteering(root, PLUGIN_SLUG) : null;
627
660
  if (cur === null || cur.body === null) return '— none: CLAUDE.md carries the block';
628
661
  const kept = localFileLinked(root, cur.path)
@@ -642,7 +675,7 @@ function localSteeringStatus(cwd) {
642
675
  }
643
676
  // Before "not a git work tree": with auto-adopt off nothing is injected either (R3-13).
644
677
  if (isAutoAdoptDisabledFor(cwd) || (root && isAutoAdoptDisabledFor(root)))
645
- return '— none: auto-adopt is off for this project (`claude-mem-lite adopt --enable` turns it back on)';
678
+ return `— none: auto-adopt is off for this project (\`${CLI_INVOKE} adopt --enable\` turns it back on)`;
646
679
  if (process.env.MEM_NO_AUTO_ADOPT === '1')
647
680
  return '— none: MEM_NO_AUTO_ADOPT=1, so no local file is written';
648
681
  if (!root)
@@ -656,10 +689,10 @@ function localSteeringStatus(cwd) {
656
689
  const plan = planLocalSteering(root, PLUGIN_SLUG, cwd);
657
690
  const beside = plan.agentsMd ?? 'the AGENTS.md';
658
691
  if (plan.file === RULES_MD && plan.refusal === 'off')
659
- return `${removed}; beside ${beside} a file comes back only with CLAUDE_MEM_RULES_STEERING=1 and then \`claude-mem-lite adopt --enable\``;
692
+ return `${removed}; beside ${beside} a file comes back only with CLAUDE_MEM_RULES_STEERING=1 and then \`${CLI_INVOKE} adopt --enable\``;
660
693
  if (plan.file === RULES_MD && plan.refusal)
661
694
  return `${removed}; beside ${beside}, ${rulesWhy(plan.refusal)}`;
662
- return `${removed}; after \`claude-mem-lite adopt --enable\` the next session may write it again`;
695
+ return `${removed}; after \`${CLI_INVOKE} adopt --enable\` the next session may write it again`;
663
696
  }
664
697
  const plan = planLocalSteering(root, PLUGIN_SLUG, cwd);
665
698
  if (!plan.refusal)
@@ -671,7 +704,7 @@ function localSteeringStatus(cwd) {
671
704
  ways.settingGivesFile
672
705
  ? 'set Project instructions to claude-md-and-agents-md in /config and CLAUDE.local.md is written instead'
673
706
  : null,
674
- ways.adoptImports ? 'run `claude-mem-lite adopt`, whose CLAUDE.md imports AGENTS.md' : null,
707
+ ways.adoptImports ? `run \`${CLI_INVOKE} adopt\`, whose CLAUDE.md imports AGENTS.md` : null,
675
708
  ].filter(Boolean);
676
709
  return `✗ not written: Claude Code stops reading ${plan.agentsMd ?? 'the AGENTS.md'} once a CLAUDE.local.md exists, and ${rulesWhy(plan.refusal)} (steering is injected at session start${alt.length ? `; ${alt.join(', or ')}` : ''})`;
677
710
  }
@@ -685,6 +718,9 @@ function currentLocalLine(root, cwd, cur, runs) {
685
718
  return `✓ ${cur.path} (behind a symbolic link; the plugin leaves it as it is)`;
686
719
  if (trackedByGit(root, rel)) return `✓ ${cur.path} (tracked by git; the plugin leaves it as it is)`;
687
720
  const plan = runs ? planLocalSteering(root, PLUGIN_SLUG, cwd) : null;
721
+ // Left in place under managed-only (writeLocalSteering, D#219), not removed.
722
+ if (plan?.refusal === 'managed-only')
723
+ return `⚠ ${cur.path} (auto-written, but CLAUDE.local.md ${LOCAL_REFUSAL_TEXT['managed-only']}; the steering is injected, and the file stays for when the setting changes)`;
688
724
  if (plan?.refusal) {
689
725
  const why =
690
726
  plan.file === LOCAL_MD
@@ -764,7 +800,7 @@ function statusAll() {
764
800
  `[adopt --status] scanned ${dirs.length} memdir(s): ${legacy} with legacy sentinel (await migration), ${disabled} auto-adopt-disabled.`,
765
801
  );
766
802
  if (legacy > 0)
767
- log('[adopt --status] run `claude-mem-lite adopt --all` to sweep legacy memory-dir sentinels now.');
803
+ log(`[adopt --status] run \`${CLI_INVOKE} adopt --all\` to sweep legacy memory-dir sentinels now.`);
768
804
 
769
805
  const known = listKnownProjectDirs();
770
806
  let adoptedCount = 0;
@@ -773,7 +809,7 @@ function statusAll() {
773
809
  `[adopt --status] known projects (~/.claude.json): ${known.length} scanned, ${adoptedCount} with a CLAUDE.md managed block or partial residue (detail doc/state).`,
774
810
  );
775
811
  if (adoptedCount > 0)
776
- log('[adopt --status] run `claude-mem-lite unadopt --all` to remove every CLAUDE.md block.');
812
+ log(`[adopt --status] run \`${CLI_INVOKE} unadopt --all\` to remove every CLAUDE.md block.`);
777
813
 
778
814
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
779
815
  const noAutoAdopt = process.env.MEM_NO_AUTO_ADOPT === '1' ? '1 (opt-out)' : 'unset';
@@ -783,7 +819,7 @@ function statusAll() {
783
819
  ` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (any install path is consent; gate is the per-project opt-out below)`,
784
820
  );
785
821
  log(` MEM_NO_AUTO_ADOPT = ${noAutoAdopt} (global escape hatch)`);
786
- log('Per-project opt-out: `claude-mem-lite adopt --disable` (run --enable to re-arm).');
822
+ log(`Per-project opt-out: \`${CLI_INVOKE} adopt --disable\` (run --enable to re-arm).`);
787
823
  }
788
824
 
789
825
  /**
@@ -852,7 +888,7 @@ function unadoptAll(args) {
852
888
  if (failures > 0) process.exitCode = 1;
853
889
  if (projectDirs.length === 0) {
854
890
  log(
855
- '[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.',
891
+ `[unadopt --all] no known projects found in ~/.claude.json — if a project was adopted but never opened in Claude Code, run \`${CLI_INVOKE} unadopt\` from inside it.`,
856
892
  );
857
893
  }
858
894
  }
package/adopt-content.mjs CHANGED
@@ -73,7 +73,6 @@ export function getDetailDoc() {
73
73
 
74
74
  > 由 \`${CLI} adopt\` 生成、随版本自动刷新;卸载用 \`${CLI} unadopt\`。
75
75
  > 精炼触发表由 SessionStart 注入会话上下文(显式 \`${CLI} adopt\` 过的项目则写在 \`CLAUDE.md\` 的 \`claude-mem-lite\` 托管块里);本文件是其展开。
76
- > 设计背景见 docs/CLAUDE-MD-STEERING-PLAN.md。
77
76
 
78
77
  > **本文下方所有命令写作 \`${CLI} <cmd>\`。** 该名字只在全局装过
79
78
  > (\`npm i -g claude-mem-lite\`)时才在 PATH 上;否则用等价的
package/claudemd.mjs CHANGED
@@ -181,6 +181,31 @@ export function holdsOnlyOwnLines(p, slug) {
181
181
  }
182
182
  }
183
183
 
184
+ // Another plugin's managed block: the same sentinel shape under any slug (code-graph-mcp writes
185
+ // `<!-- code-graph-mcp:begin v2 -->` … `<!-- code-graph-mcp:end -->`). The backreference pairs a
186
+ // begin with its own slug's end, so an unpaired line of one slug never closes another's block.
187
+ const ANY_BLOCK_RE_G =
188
+ /<!-- ([a-z0-9][a-z0-9._-]*):begin v\d+ -->\r?\n(?:(?!<!-- \1:(?:begin|end))[\s\S])*?\r?\n<!-- \1:end -->/g;
189
+
190
+ /**
191
+ * Whether the instructions file at `p` holds no line a person wrote: this plugin's blocks and
192
+ * imports, and other plugins' managed blocks. For "did plugin text alone switch AGENTS.md off"
193
+ * (D#220), never for deleting the file — another plugin's block is not ours to remove, and
194
+ * holdsOnlyOwnLines answers that question. False for a missing file.
195
+ * @param {string} p absolute path
196
+ * @param {string} slug
197
+ * @returns {boolean}
198
+ */
199
+ export function holdsOnlyPluginLines(p, slug) {
200
+ try {
201
+ if (!existsSync(p)) return false;
202
+ const rest = readFileSync(p, 'utf8').replace(blockRegexG(slug), '').replace(ANY_BLOCK_RE_G, '');
203
+ return rest.trim() === '' || isOwnImportsOnly(rest, slug);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
184
209
  /**
185
210
  * Put adopt's AGENTS.md imports at the top of <cwd>/CLAUDE.md (created if absent), unless the
186
211
  * marker is there already. `imports` are paths relative to `cwd` (`AGENTS.md`,
@@ -365,7 +390,8 @@ export function needsRefresh(cwd, { slug, version, block, doc }) {
365
390
  * CLAUDE.md is created if absent. Only our slug-scoped region is rewritten;
366
391
  * everything else in the file is preserved verbatim.
367
392
  *
368
- * @returns {{action: 'created'|'updated'|'unchanged'}} (block disposition)
393
+ * @returns {{action: 'created'|'updated'|'unchanged', docAction: 'created'|'updated'|'unchanged'}}
394
+ * (block disposition, and the detail doc's)
369
395
  */
370
396
  export function writeManaged(cwd, { slug, version, block, doc }) {
371
397
  const { action } = writeBlockAt(claudeMdPath(cwd), { slug, version, block });
@@ -378,6 +404,7 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
378
404
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
379
405
  const existingDoc = existsSync(dp) ? readFileSync(dp, 'utf8') : null;
380
406
  if (existingDoc !== docContent) atomicWrite(dp, docContent);
407
+ const docAction = existingDoc === null ? 'created' : existingDoc === docContent ? 'unchanged' : 'updated';
381
408
 
382
409
  // Only when what the sidecar records changed: it is committed alongside the block in a team
383
410
  // repo, and a fresh writtenAt on every re-run of `adopt` left a diff each time (E2E
@@ -393,7 +420,7 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
393
420
  if (!(prev?.version === version && prev?.blockHash === blockHash && prev?.docHash === docHash)) {
394
421
  writeState(cwd, slug, { version, blockHash, docHash, writtenAt: new Date().toISOString() });
395
422
  }
396
- return { action };
423
+ return { action, docAction };
397
424
  }
398
425
 
399
426
  /**
package/cli/activity.mjs CHANGED
@@ -15,6 +15,7 @@ import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
15
15
  import { parseIntFlag, isNumericToken } from '../lib/cli-flags.mjs';
16
16
  import { snapshotDb } from '../lib/db-backup.mjs';
17
17
  import { isBlankText } from '../lib/blank-text.mjs';
18
+ import { CLI_INVOKE } from '../cli-path.mjs';
18
19
 
19
20
  function formatActivityResults(rows) {
20
21
  if (!rows || rows.length === 0) return '(no events)';
@@ -31,7 +32,7 @@ function eventIdOf(token) {
31
32
  export async function cmdActivity(db, args) {
32
33
  const sub = args[0];
33
34
  if (!sub) {
34
- fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show|delete|promote> ...');
35
+ fail(`[mem] Usage: ${CLI_INVOKE} activity <save|search|recent|show|delete|promote> ...`);
35
36
  return;
36
37
  }
37
38
 
@@ -171,7 +172,7 @@ export async function cmdActivity(db, args) {
171
172
  // This command lets users prune them by ID without dropping to raw SQL.
172
173
  const idStr = positional.join(',').trim();
173
174
  if (!idStr) {
174
- fail('[mem] Usage: claude-mem-lite activity delete <id1,id2,...> [--confirm]');
175
+ fail(`[mem] Usage: ${CLI_INVOKE} activity delete <id1,id2,...> [--confirm]`);
175
176
  return;
176
177
  }
177
178
  const tokens = idStr
package/cli/common.mjs CHANGED
@@ -9,7 +9,8 @@
9
9
  // consistent.
10
10
 
11
11
  import { editDistance } from '../lib/edit-distance.mjs';
12
- import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from '../format-utils.mjs';
12
+ import { neutralizeContextDelimiters, neutralizeSkillDelimiters, truncate } from '../format-utils.mjs';
13
+ import { CLI_INVOKE } from '../cli-path.mjs';
13
14
 
14
15
  // ─── Argument Parsing ────────────────────────────────────────────────────────
15
16
 
@@ -250,7 +251,7 @@ export function inertFilterFlagNotice(cmd, inert) {
250
251
  const verb = inert.length > 1 ? 'were' : 'was';
251
252
  return (
252
253
  `[mem] ${names} ${verb} ignored — \`${cmd}\` does not filter on ${inert.length > 1 ? 'them' : 'it'}, ` +
253
- `so the results above are UNFILTERED. Run "claude-mem-lite help" for the flags this command reads.`
254
+ `so the results above are UNFILTERED. Run "${CLI_INVOKE} help" for the flags this command reads.`
254
255
  );
255
256
  }
256
257
 
@@ -647,3 +648,28 @@ export function formatProbeHints(probe) {
647
648
  if (probe.event?.length > 0) hints.push(`E#${probe.event.join(', E#')} (event)`);
648
649
  return hints;
649
650
  }
651
+
652
+ /**
653
+ * The line under one side of a scan's Needs-review pair (D#461), from reviewRows: type, project,
654
+ * date, the lesson or that there is none, and the start of the narrative — enough to tell two rows
655
+ * with near-identical titles apart without `get`, which counts as an access.
656
+ * @param {{id: number, type?: string, project?: string, created_at_epoch?: number,
657
+ * narrative?: string|null, lesson_learned?: string|null}|undefined} row
658
+ * @returns {string}
659
+ */
660
+ export function formatReviewSide(row) {
661
+ if (!row) return '';
662
+ const flat = (v, n) =>
663
+ truncate(
664
+ String(v ?? '')
665
+ .replace(/\s+/g, ' ')
666
+ .trim(),
667
+ n,
668
+ );
669
+ const lesson =
670
+ row.lesson_learned && row.lesson_learned !== 'none'
671
+ ? `lesson: ${flat(row.lesson_learned, 100)}`
672
+ : 'no lesson';
673
+ const date = row.created_at_epoch ? new Date(row.created_at_epoch).toISOString().slice(0, 10) : '?';
674
+ return `#${row.id} ${row.type || '?'} · ${row.project} · ${date} · ${lesson} · ${flat(row.narrative, 120) || '(no narrative)'}`;
675
+ }
package/cli/fts-check.mjs CHANGED
@@ -3,12 +3,13 @@
3
3
 
4
4
  import { checkFTSIntegrity, rebuildFTS } from '../schema.mjs';
5
5
  import { parseArgs, out, fail } from './common.mjs';
6
+ import { CLI_INVOKE } from '../cli-path.mjs';
6
7
 
7
8
  export function cmdFtsCheck(db, args) {
8
9
  const { positional } = parseArgs(args);
9
10
  const action = positional[0];
10
11
  if (!action) {
11
- fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
12
+ fail(`[mem] Usage: ${CLI_INVOKE} fts-check <check|rebuild>`);
12
13
  return;
13
14
  }
14
15
  if (!['check', 'rebuild'].includes(action)) {
@@ -23,12 +23,12 @@ import {
23
23
  markUndone,
24
24
  } from '../lib/verify-apply-core.mjs';
25
25
  import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
26
- import { shellWord } from '../cli-path.mjs';
26
+ import { shellWord, CLI_INVOKE } from '../cli-path.mjs';
27
27
 
28
28
  const USAGE =
29
- '[mem] Usage: claude-mem-lite verify-apply <proposals.json> [--project P] (dry run)\n' +
30
- ' claude-mem-lite verify-apply <proposals.json> [--project P] --apply --digest <d>\n' +
31
- ' claude-mem-lite verify-apply --undo <backup.json>';
29
+ `[mem] Usage: ${CLI_INVOKE} verify-apply <proposals.json> [--project P] (dry run)\n` +
30
+ ` ${CLI_INVOKE} verify-apply <proposals.json> [--project P] --apply --digest <d>\n` +
31
+ ` ${CLI_INVOKE} verify-apply --undo <backup.json>`;
32
32
 
33
33
  const SNIPPET_CONTEXT = 60;
34
34
  const BACKUP_DIR = join(DB_DIR, 'backups');
package/cli.mjs CHANGED
@@ -338,8 +338,12 @@ if (cmd === '--version' || cmd === '-v' || cmd === '-V' || cmd === 'version') {
338
338
  if (best && bestDist <= 2) {
339
339
  process.stderr.write(`[mem] Did you mean: ${best}?\n`);
340
340
  } else {
341
+ // By path (D#462: the bare name is on PATH only after a global npm install). Imported here,
342
+ // not at the top: this entry takes no static local imports (D#26), and a module it cannot
343
+ // load leaves the old wording rather than no hint.
344
+ const { CLI_INVOKE } = await import('./cli-path.mjs').catch(() => ({ CLI_INVOKE: 'claude-mem-lite' }));
341
345
  process.stderr.write(
342
- '[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n',
346
+ `[mem] Run "${CLI_INVOKE} help" for CLI commands or "${CLI_INVOKE} install" for setup\n`,
343
347
  );
344
348
  }
345
349
  process.exitCode = 1;
package/commands/adopt.md CHANGED
@@ -59,10 +59,10 @@ doc). Other plugins' blocks (e.g. `code-graph-mcp:*`) and your own prose survive
59
59
  - `/unadopt` removes the CLAUDE.md block + `.claude/` detail doc (your prose stays),
60
60
  and the local block. The next session writes a local file if the plugin never
61
61
  created one in this repository; one it created before is not written back (the
62
- text is injected instead) until `claude-mem-lite adopt --enable`. Beside an
62
+ text is injected instead) until `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt --enable`. Beside an
63
63
  `AGENTS.md` that `CLAUDE.local.md` would switch off, no file is written unless
64
64
  `CLAUDE_MEM_RULES_STEERING=1`.
65
- - `claude-mem-lite adopt --disable` permanently stops auto-adopt for this project
65
+ - `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt --disable` permanently stops auto-adopt for this project
66
66
  (also for sessions started in its subdirectories) and removes the local block; a
67
67
  `CLAUDE.md` block stays until `/unadopt`.
68
68
  - `MEM_NO_AUTO_ADOPT=1` disables auto-adopt globally; blocks already written stay.
@@ -43,10 +43,10 @@ project (slug-scoped — other plugins' blocks survive).
43
43
 
44
44
  `/unadopt` removes files, not the steering. A local block the plugin created is not
45
45
  written back; the text is added to each session's context instead
46
- (`claude-mem-lite adopt --enable` lets it write the file again). A project whose
46
+ (`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt --enable` lets it write the file again). A project whose
47
47
  `CLAUDE.md` block you removed gets a local file on the next session, unless the
48
48
  plugin created one there before. Beside an `AGENTS.md` that `CLAUDE.local.md` would
49
- switch off, no file is written unless `CLAUDE_MEM_RULES_STEERING=1`. To stop the steering: `claude-mem-lite adopt --disable`
49
+ switch off, no file is written unless `CLAUDE_MEM_RULES_STEERING=1`. To stop the steering: `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt --disable`
50
50
  (per project) or `MEM_NO_AUTO_ADOPT=1` (global).
51
51
 
52
52
  ## Aftermath
@@ -24,7 +24,7 @@ Run each command below via Bash; the CLI reaches the maintenance operations whet
24
24
  2. Report scan results to the user (duplicates, stale items, broken items, boostable items, **pending purge** items)
25
25
  3. **If pending purge items > 0**: first run the purge command below without `--confirm`: it prints how many rows it would delete and deletes nothing. If that count is 0, tell the user nothing is old enough to purge yet and go on to the next step. Otherwise report that count to the user and ask for confirmation. If confirmed, run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops purge_stale --confirm`. **`--confirm` is required:** without it the command prints a dry-run preview and deletes nothing while still succeeding, so you would report a purge that never happened. User may optionally add `--retain-days N` (default 30, allowed range 7–365) to both runs to control how many days of data to keep; the CLI rejects a value outside that range, so tell the user instead of substituting the default. Do NOT purge without explicit user confirmation. The purge comes before the next step on purpose: decay there queues more rows for purge, and those wait for a later run instead of joining the purge the user approved.
26
26
  4. Run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute` to apply safe automatic changes — with no `--ops` it runs the default set: cleanup, decay, boost, then demote_pinned (left out when `CLAUDE_MEM_SKIP_DEMOTE_PINNED=1`)
27
- 5. If duplicates were found in scan, review them and run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops dedup --merge-ids keepId:removeId,...` — keep the more important/recent observation in each pair. Groups are comma-separated and the first id in each is kept (`10:11,20:21:22`). For pairs similar enough to merge without review the scan prints a ready-to-run command after `Ready-to-use:`; run it as printed
27
+ 5. If duplicates were found in scan, review them and run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" maintain execute --ops dedup --merge-ids keepId:removeId,...` — keep the more important/recent observation in each pair. Groups are comma-separated and the first id in each is kept (`10:11,20:21:22`). For pairs similar enough to merge without review the scan prints a ready-to-run command after `Ready-to-use:`; run it as printed. Judge each pair under `Needs review:` from the two lines the scan prints below it; do not read them with `get`: that counts as an access and can raise a row's importance
28
28
  6. Run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" compress --execute` for old low-value observations (without `--execute` it only previews)
29
29
 
30
30
  ### Phase 2: Summary
package/haiku-client.mjs CHANGED
@@ -832,7 +832,11 @@ function claudeArgs(modelName) {
832
832
  * their output tokens and latency (measured above, 2026-09-29; the model the CLI ran then was
833
833
  * not recorded). Claude Code 2.1.301 does not change its request for Claude Haiku 5.5 on this
834
834
  * variable: the body carries adaptive thinking either way (D#334). Earlier versions were not
835
- * checked.
835
+ * checked. CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 drops Claude Code's auto-memory section, about 2.2 KB
836
+ * of every system prompt that tells the model about a memory directory these calls never use
837
+ * (stub-probed on 2.1.304: system 8,964 -> 6,688 B). The 2026-10-20 A/B on the cli leg, gate set
838
+ * before the run (D#483): usable 8/8, 8/8, 7/8 against the same without it; kept lessons 3 vs 4
839
+ * at n=8, inside floor(0.9 x 4).
836
840
  */
837
841
  function cliSpawnEnv() {
838
842
  return {
@@ -840,6 +844,7 @@ function cliSpawnEnv() {
840
844
  CLAUDE_MEM_HOOK_RUNNING: '1',
841
845
  DISABLE_CLAUDEMD_HOOKS: '1',
842
846
  MAX_THINKING_TOKENS: '0',
847
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
843
848
  };
844
849
  }
845
850
 
package/hook-llm.mjs CHANGED
@@ -93,15 +93,19 @@ export function retractPreSavedObs(db, obsId, where) {
93
93
  // Both statements run in one transaction so a crash between them cannot leave that state
94
94
  // either; nesting under persistHaikuSummary's transaction is safe (better-sqlite3 uses
95
95
  // savepoints).
96
- return db.transaction(() => {
97
- const live = db.prepare(`SELECT 1 FROM observations WHERE id = ? AND ${liveObsFilterSql('')}`).get(obsId);
98
- if (!live) {
99
- debugLog('DEBUG', 'llm-episode', `${where}: pre-saved obs #${obsId} no longer live — left in place`);
100
- return false;
101
- }
102
- recoverChildrenOf(db, [obsId]);
103
- return db.prepare('DELETE FROM observations WHERE id = ?').run(obsId).changes > 0;
104
- })();
96
+ return db
97
+ .transaction(() => {
98
+ const live = db
99
+ .prepare(`SELECT 1 FROM observations WHERE id = ? AND ${liveObsFilterSql('')}`)
100
+ .get(obsId);
101
+ if (!live) {
102
+ debugLog('DEBUG', 'llm-episode', `${where}: pre-saved obs #${obsId} no longer live — left in place`);
103
+ return false;
104
+ }
105
+ recoverChildrenOf(db, [obsId]);
106
+ return db.prepare('DELETE FROM observations WHERE id = ?').run(obsId).changes > 0;
107
+ })
108
+ .immediate();
105
109
  }
106
110
  // T9: memdir-incompatible types live in the `events` table, not `observations`.
107
111
  // Set lookup is O(1) — authoritative source is lib/activity.mjs::EVENT_TYPES.
@@ -544,13 +548,15 @@ export function persistHaikuSummary(db, summary, ctx) {
544
548
  });
545
549
 
546
550
  if (ctx.preSavedObsId) {
547
- const id = db.transaction(() => {
548
- // Same live-row guard as the in-place upgrade (FLOW-7); the event gets written
549
- // either way. Shared helper since audit 2026-09-02 P0-6 — this was the only one
550
- // of the three retraction sites that carried the guard.
551
- retractPreSavedObs(db, ctx.preSavedObsId, 'upgrade-delete');
552
- return insertEvent();
553
- })();
551
+ const id = db
552
+ .transaction(() => {
553
+ // Same live-row guard as the in-place upgrade (FLOW-7); the event gets written
554
+ // either way. Shared helper since audit 2026-09-02 P0-6 — this was the only one
555
+ // of the three retraction sites that carried the guard.
556
+ retractPreSavedObs(db, ctx.preSavedObsId, 'upgrade-delete');
557
+ return insertEvent();
558
+ })
559
+ .immediate();
554
560
  return { table: 'events', id };
555
561
  }
556
562