claude-mem-lite 3.12.1 → 3.13.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": "3.12.1",
13
+ "version": "3.13.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.12.1",
3
+ "version": "3.13.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
@@ -1,25 +1,32 @@
1
- // Phase C (Invited-Memory plan, T11): CLI handlers for
2
- // claude-mem-lite adopt [--all] [--force] [--dry-run] [--status]
3
- // claude-mem-lite unadopt [--all]
1
+ // CLAUDE.md-steering plan (v3.13): CLI handlers for
2
+ // claude-mem-lite adopt [--all] [--force] [--dry-run] [--status] [--disable|--enable]
3
+ // claude-mem-lite unadopt [--all] [--force] [--dry-run] [--status]
4
4
  //
5
- // adopt = write sentinel section into MEMORY.md + drop plugin_claude_mem_lite.md
6
- // unadopt = precise sentinel removal + doc cleanup
7
- // --all = scan every project under ~/.claude/projects/*/memory/
8
- // --force = override UserEditedError
9
- // --dry-run = print intent without writing
10
- // --status = list all adopted projects + versions
5
+ // adopt = write the managed block into <cwd>/CLAUDE.md + drop
6
+ // <cwd>/.claude/plugin_claude_mem_lite.md, and migrate this project's
7
+ // legacy memory-dir sentinel away.
8
+ // unadopt = remove the CLAUDE.md block + detail doc (and clean any legacy residue).
9
+ //
10
+ // The project path is needed to write CLAUDE.md, but the per-project memdir slug
11
+ // (~/.claude/projects/<encoded>/) is a LOSSY encoding of the real cwd — it cannot
12
+ // be decoded back to a filesystem path. So `--all` cannot adopt arbitrary projects;
13
+ // it is redefined as a legacy-cleanup sweep (strip old memory-dir sentinels across
14
+ // every memdir). New-scheme adoption happens per-project on SessionStart (cwd known).
11
15
 
12
16
  import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
13
17
  import { homedir } from 'os';
14
18
  import { join } from 'path';
15
19
  import {
16
- memdirPath, writePluginSection, removePluginSection,
17
- writePluginDoc, removePluginDoc,
18
- isAdopted, hasPluginState, readMemoryIndex,
19
- UserEditedError, BudgetExceededError,
20
+ memdirPath, removePluginSection, removePluginDoc,
21
+ isAdopted as memdirIsAdopted, hasPluginState,
20
22
  } from './memdir.mjs';
21
23
  import {
22
- PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, getIndexLine, getDetailDoc,
24
+ writeManaged, removeManaged, isAdopted as claudeMdIsAdopted,
25
+ needsRefresh, migrateLegacyMemoryDir, hasLegacyMemdirSentinel,
26
+ claudeMdPath, detailDocPath,
27
+ } from './claudemd.mjs';
28
+ import {
29
+ PLUGIN_SLUG, CURRENT_SENTINEL_VERSION, buildClaudeMdBlock, getDetailDoc,
23
30
  } from './adopt-content.mjs';
24
31
 
25
32
  function log(msg) { console.log(msg); }
@@ -56,7 +63,8 @@ function hasFlag(args, flag) { return Array.isArray(args) && args.includes(flag)
56
63
  // `rm ~/.claude-mem-lite/runtime/.auto-adopt-*`. Managed via
57
64
  // `claude-mem-lite adopt --disable` / `--enable`. silentAutoAdopt checks it
58
65
  // at entry and skips WITHOUT writing the runtime marker, so toggling
59
- // `--enable` re-arms auto-adopt on the next SessionStart.
66
+ // `--enable` re-arms auto-adopt on the next SessionStart. Kept in the memdir
67
+ // (not the project tree) so it survives `unadopt` cleaning out .claude/.
60
68
  const DISABLE_SENTINEL_BASENAME = '.mem-no-auto-adopt';
61
69
 
62
70
  export function disableSentinelPath(memdir) {
@@ -68,97 +76,98 @@ export function isAutoAdoptDisabled(memdir) {
68
76
  }
69
77
 
70
78
  /**
71
- * cmdAdopt — write sentinel section + plugin doc to memdir.
72
- * Exit code 1 on any hard failure; skipped (--all + UserEditedError) doesn't
73
- * fail the batch.
79
+ * cmdAdopt — write the CLAUDE.md managed block + detail doc for the current
80
+ * project, and migrate its legacy memory-dir sentinel away.
81
+ *
82
+ * `--all` does NOT adopt every project (their real paths are unrecoverable from
83
+ * the lossy memdir slug) — it sweeps the legacy memory-dir cleanup across all
84
+ * memdirs. `--status`/`--disable`/`--enable` as before.
74
85
  */
75
86
  export function cmdAdopt(args = []) {
76
87
  if (hasFlag(args, '--status')) return statusAll();
77
88
  if (hasFlag(args, '--disable')) return cmdDisable(args);
78
89
  if (hasFlag(args, '--enable')) return cmdEnable(args);
90
+ if (hasFlag(args, '--all')) return migrateAll(args);
79
91
 
80
- const all = hasFlag(args, '--all');
81
92
  const force = hasFlag(args, '--force');
82
93
  const dryRun = hasFlag(args, '--dry-run');
94
+ const cwd = detectCwd();
83
95
 
84
- const targets = all
85
- ? listAllMemdirs().map((m) => m.memdir)
86
- : [memdirPath(detectCwd())];
87
-
88
- if (targets.length === 0) {
89
- log('[adopt] no memdirs to adopt (use without --all for current project)');
90
- return;
91
- }
92
-
93
- let created = 0, updated = 0, unchanged = 0, skipped = 0, failed = 0;
94
- for (const memdir of targets) {
95
- const r = adoptOne(memdir, { force, dryRun, all });
96
- if (r.action === 'created') created++;
97
- else if (r.action === 'updated') updated++;
98
- else if (r.action === 'unchanged') unchanged++;
99
- else if (r.action === 'skipped') skipped++;
100
- else if (r.action === 'dry-run') unchanged++;
101
- else failed++;
102
- }
103
-
104
- log('');
105
- log(`[adopt] ${targets.length} target(s): ${created} created, ${updated} updated, ${unchanged} unchanged, ${skipped} skipped, ${failed} failed`);
106
- if (failed > 0) process.exitCode = 1;
96
+ adoptOne(cwd, { force, dryRun });
107
97
  }
108
98
 
109
- function adoptOne(memdir, { force, dryRun, all }) {
110
- const contentLine = getIndexLine();
99
+ function adoptOne(cwd, { force, dryRun }) {
100
+ const block = buildClaudeMdBlock();
101
+ const doc = getDetailDoc();
111
102
  const version = CURRENT_SENTINEL_VERSION;
112
103
 
113
104
  if (dryRun) {
114
- log(`[adopt --dry-run] ${memdir}`);
115
- log(` MEMORY.md line: ${contentLine}`);
116
- log(` detail file: plugin_claude_mem_lite.md (${getDetailDoc().length} chars)`);
105
+ log(`[adopt --dry-run] ${cwd}`);
106
+ log(` CLAUDE.md block: ${claudeMdPath(cwd)} (${block.length} chars, ${version})`);
107
+ log(` detail doc: ${detailDocPath(cwd, PLUGIN_SLUG)} (${doc.length} chars)`);
108
+ if (hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG)) {
109
+ log(` legacy migrate: would strip memory-dir sentinel @ ${memdirPath(cwd)}`);
110
+ }
117
111
  return { action: 'dry-run' };
118
112
  }
119
113
 
120
114
  try {
121
- const r = writePluginSection(memdir, { slug: PLUGIN_SLUG, version, contentLine, force });
122
- writePluginDoc(memdir, PLUGIN_SLUG, getDetailDoc());
123
- log(`[adopt] ${memdir} ${r.action}`);
115
+ const mig = migrateLegacyMemoryDir(cwd, PLUGIN_SLUG, { force });
116
+ const r = writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
117
+ const migNote = mig.action === 'removed' ? ' (+migrated legacy memdir)' : '';
118
+ log(`[adopt] ${cwd} → ${r.action}${migNote}`);
124
119
  return r;
125
120
  } catch (e) {
126
- if (e instanceof UserEditedError && all) {
127
- log(`[adopt] ${memdir} → skipped (user-edited; pass --force to override)`);
128
- return { action: 'skipped' };
129
- }
130
- if (e instanceof UserEditedError) {
131
- log(`[adopt] ${memdir} → refused: ${e.message}`);
132
- log('[adopt] pass --force to overwrite, or edit/uninstall manually.');
133
- return { action: 'failed' };
134
- }
135
- if (e instanceof BudgetExceededError) {
136
- log(`[adopt] ${memdir} → failed: ${e.message}`);
137
- return { action: 'failed' };
138
- }
139
- log(`[adopt] ${memdir} → error: ${e.message}`);
121
+ log(`[adopt] ${cwd} error: ${e.message}`);
122
+ process.exitCode = 1;
140
123
  return { action: 'failed' };
141
124
  }
142
125
  }
143
126
 
144
127
  /**
145
- * silentAutoAdoptplugin-mode first-run auto-adopt helper (v2.33.0+).
146
- *
147
- * Preconditions (caller must gate): CLAUDE_PLUGIN_ROOT set, MEM_NO_AUTO_ADOPT!=1,
148
- * first-attempt marker absent. This helper does NOT re-check those it only
149
- * does the write + marker persistence. (v2.82.0: dropped MEM_QUIET_HOOKS gate;
150
- * quiet is a stdout control, not a side-effect control.)
151
- *
152
- * Behavior:
153
- * - If `<memdir>/.mem-no-auto-adopt` exists: skip silently, do NOT write the
154
- * runtime marker. This keeps `--enable` re-armable: deleting the disable
155
- * sentinel lets the next SessionStart try again.
156
- * - Else: writes plugin sentinel + detail doc to the memdir for `cwd`.
157
- * - Writes a per-project first-attempt marker under `markerDir` so a later
158
- * `/unadopt` is respected (no re-adopt loop).
159
- * - Silent: never logs, never throws. Returns structured result.
128
+ * migrateAll`claude-mem-lite adopt --all`: legacy-cleanup sweep. Strips the
129
+ * old memory-dir sentinel + detail doc from every memdir. Does NOT write any
130
+ * CLAUDE.md block (target paths are unrecoverable) that happens per-project on
131
+ * the next SessionStart. Respects the foreign-content guard unless --force.
132
+ */
133
+ function migrateAll(args) {
134
+ const force = hasFlag(args, '--force');
135
+ const dryRun = hasFlag(args, '--dry-run');
136
+ const dirs = listAllMemdirs();
137
+ if (dirs.length === 0) { log('[adopt --all] no memdirs found'); return; }
138
+
139
+ let removed = 0, absent = 0, skipped = 0;
140
+ for (const { projectSlug, memdir } of dirs) {
141
+ if (dryRun) {
142
+ const has = memdirIsAdopted(memdir, PLUGIN_SLUG);
143
+ const action = !has ? 'absent'
144
+ : (hasPluginState(memdir, PLUGIN_SLUG) || force) ? 'would-remove' : 'would-skip-foreign';
145
+ log(`[adopt --all --dry-run] ${projectSlug} → ${action}`);
146
+ if (action === 'would-remove') removed++;
147
+ else if (action === 'would-skip-foreign') skipped++;
148
+ else absent++;
149
+ continue;
150
+ }
151
+ const r = removePluginSection(memdir, PLUGIN_SLUG, { force });
152
+ if (r.action === 'removed') { removePluginDoc(memdir, PLUGIN_SLUG); removed++; }
153
+ else if (r.action === 'skipped-foreign') skipped++;
154
+ else absent++;
155
+ }
156
+ log('');
157
+ log(`[adopt --all] legacy memory-dir cleanup over ${dirs.length} project(s): ${removed} cleaned, ${skipped} skipped-foreign, ${absent} none.`);
158
+ log('[adopt --all] CLAUDE.md adoption is per-project — it runs automatically on each project\'s next SessionStart.');
159
+ }
160
+
161
+ /**
162
+ * silentAutoAdopt — SessionStart idempotent sync (migration vehicle).
160
163
  *
161
- * Returns { ok, action, reason } caller uses for telemetry / debugLog only.
164
+ * Called every plugin-mode SessionStart (NOT gated by the one-shot marker, so
165
+ * existing users whose marker predates v3.13 still migrate). Order:
166
+ * 1. respect per-project `.mem-no-auto-adopt` opt-out → skip.
167
+ * 2. migrate legacy memory-dir sentinel away (idempotent; no-op once gone).
168
+ * 3. adopt the CLAUDE.md scheme if absent; else refresh if shipped content
169
+ * drifted (unless CLAUDE_MEM_NO_TEMPLATE_REFRESH=1).
170
+ * Silent: never logs, never throws. Returns { ok, action, reason } for debugLog.
162
171
  */
163
172
  export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
164
173
  const memdir = memdirPath(cwd);
@@ -166,27 +175,26 @@ export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
166
175
  if (isAutoAdoptDisabled(memdir)) {
167
176
  return { ok: true, action: 'disabled', reason: 'disabled-by-sentinel' };
168
177
  }
169
- if (isAdopted(memdir, PLUGIN_SLUG)) {
170
- writeMarker(markerDir, markerKey);
171
- return { ok: true, action: 'already-adopted' };
178
+ migrateLegacyMemoryDir(cwd, PLUGIN_SLUG);
179
+
180
+ const block = buildClaudeMdBlock();
181
+ const doc = getDetailDoc();
182
+ const version = CURRENT_SENTINEL_VERSION;
183
+
184
+ let action = 'already-adopted';
185
+ if (!claudeMdIsAdopted(cwd, PLUGIN_SLUG)) {
186
+ writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
187
+ action = 'adopted';
188
+ } else if (process.env.CLAUDE_MEM_NO_TEMPLATE_REFRESH !== '1'
189
+ && needsRefresh(cwd, { slug: PLUGIN_SLUG, version, block, doc })) {
190
+ writeManaged(cwd, { slug: PLUGIN_SLUG, version, block, doc });
191
+ action = 'refreshed';
172
192
  }
173
- writePluginSection(memdir, {
174
- slug: PLUGIN_SLUG,
175
- version: CURRENT_SENTINEL_VERSION,
176
- contentLine: getIndexLine(),
177
- force: false,
178
- });
179
- writePluginDoc(memdir, PLUGIN_SLUG, getDetailDoc());
180
- writeMarker(markerDir, markerKey);
181
- return { ok: true, action: 'adopted' };
193
+ if (markerDir && markerKey) writeMarker(markerDir, markerKey);
194
+ return { ok: true, action };
182
195
  } catch (e) {
183
- // Budget exceeded, user-edited conflict, or FS error write marker so we
184
- // don't retry on every SessionStart. User can run /adopt --force manually.
185
- try { writeMarker(markerDir, markerKey); } catch { /* marker best-effort */ }
186
- const reason = e instanceof UserEditedError ? 'user-edited'
187
- : e instanceof BudgetExceededError ? 'budget-exceeded'
188
- : 'error';
189
- return { ok: false, action: 'skipped', reason, err: e };
196
+ try { if (markerDir && markerKey) writeMarker(markerDir, markerKey); } catch { /* best-effort */ }
197
+ return { ok: false, action: 'skipped', reason: 'error', err: e };
190
198
  }
191
199
  }
192
200
 
@@ -202,13 +210,8 @@ export function hasAutoAdoptMarker(markerDir, markerKey) {
202
210
 
203
211
  /**
204
212
  * cmdDisable — `claude-mem-lite adopt --disable [--all]`.
205
- *
206
213
  * Writes `<memdir>/.mem-no-auto-adopt` so SessionStart auto-adopt skips this
207
- * project permanently. Idempotent: re-running on an already-disabled memdir is
208
- * a no-op. Does NOT remove an existing sentinel — pair with `unadopt` if you
209
- * want both. The two operations are deliberately separate:
210
- * - `unadopt` = "remove the contract now"
211
- * - `adopt --disable` = "and don't auto-write it back"
214
+ * project permanently. Does NOT remove an existing block — pair with `unadopt`.
212
215
  */
213
216
  function cmdDisable(args) {
214
217
  const all = hasFlag(args, '--all');
@@ -216,10 +219,7 @@ function cmdDisable(args) {
216
219
  ? listAllMemdirs().map((m) => m.memdir)
217
220
  : [memdirPath(detectCwd())];
218
221
 
219
- if (targets.length === 0) {
220
- log('[adopt --disable] no memdirs found');
221
- return;
222
- }
222
+ if (targets.length === 0) { log('[adopt --disable] no memdirs found'); return; }
223
223
 
224
224
  let disabled = 0, already = 0;
225
225
  for (const memdir of targets) {
@@ -234,17 +234,13 @@ function cmdDisable(args) {
234
234
  log(`[adopt --disable] ${memdir} → disabled`);
235
235
  disabled++;
236
236
  }
237
-
238
237
  log('');
239
238
  log(`[adopt --disable] ${targets.length} target(s): ${disabled} newly disabled, ${already} already disabled`);
240
239
  }
241
240
 
242
241
  /**
243
- * cmdEnable — `claude-mem-lite adopt --enable [--all]`.
244
- *
245
- * Removes the `<memdir>/.mem-no-auto-adopt` sentinel so the next SessionStart
246
- * can auto-adopt again. Idempotent. Does NOT trigger an immediate adoption —
247
- * run plain `claude-mem-lite adopt` if you want that now.
242
+ * cmdEnable — `claude-mem-lite adopt --enable [--all]`. Removes the
243
+ * `.mem-no-auto-adopt` sentinel so the next SessionStart can auto-adopt again.
248
244
  */
249
245
  function cmdEnable(args) {
250
246
  const all = hasFlag(args, '--all');
@@ -252,10 +248,7 @@ function cmdEnable(args) {
252
248
  ? listAllMemdirs().map((m) => m.memdir)
253
249
  : [memdirPath(detectCwd())];
254
250
 
255
- if (targets.length === 0) {
256
- log('[adopt --enable] no memdirs found');
257
- return;
258
- }
251
+ if (targets.length === 0) { log('[adopt --enable] no memdirs found'); return; }
259
252
 
260
253
  let enabled = 0, absent = 0;
261
254
  for (const memdir of targets) {
@@ -269,56 +262,48 @@ function cmdEnable(args) {
269
262
  log(`[adopt --enable] ${memdir} → enabled`);
270
263
  enabled++;
271
264
  }
272
-
273
265
  log('');
274
266
  log(`[adopt --enable] ${targets.length} target(s): ${enabled} re-enabled, ${absent} not-disabled`);
275
267
  }
276
268
 
269
+ /**
270
+ * statusAll — report the current project's new-scheme adoption, plus a sweep of
271
+ * how many memdirs still carry the legacy sentinel (i.e. await migration).
272
+ */
277
273
  function statusAll() {
274
+ const cwd = detectCwd();
275
+ const adoptedHere = claudeMdIsAdopted(cwd, PLUGIN_SLUG);
276
+ log('[adopt --status] current project:');
277
+ log(` cwd: ${cwd}`);
278
+ log(` CLAUDE.md: ${adoptedHere ? `✓ adopted (${CURRENT_SENTINEL_VERSION})` : '✗ not adopted'}`);
279
+ if (hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG)) {
280
+ log(' legacy: ⚠ memory-dir sentinel still present (migrates on next SessionStart, or run `adopt`)');
281
+ }
282
+
278
283
  const dirs = listAllMemdirs();
279
- log('[adopt --status] scanning ~/.claude/projects/*/memory/');
280
- if (dirs.length === 0) { log(' (no memdirs found)'); return; }
281
- let adopted = 0, disabled = 0;
282
- for (const { projectSlug, memdir } of dirs) {
283
- const isAdoptedHere = isAdopted(memdir, PLUGIN_SLUG);
284
- const isDisabledHere = isAutoAdoptDisabled(memdir);
285
- if (isAdoptedHere) {
286
- const idx = readMemoryIndex(memdir, PLUGIN_SLUG);
287
- const suffix = isDisabledHere ? ' [auto-adopt disabled]' : '';
288
- log(` ✓ ${projectSlug} (${idx.version})${suffix}`);
289
- adopted++;
290
- if (isDisabledHere) disabled++;
291
- } else if (isDisabledHere) {
292
- log(` ✗ ${projectSlug} (auto-adopt disabled, no sentinel)`);
293
- disabled++;
294
- }
284
+ let legacy = 0, disabled = 0;
285
+ for (const { memdir } of dirs) {
286
+ if (memdirIsAdopted(memdir, PLUGIN_SLUG)) legacy++;
287
+ if (isAutoAdoptDisabled(memdir)) disabled++;
295
288
  }
296
289
  log('');
297
- log(`[adopt --status] ${adopted}/${dirs.length} adopted${disabled > 0 ? `, ${disabled} disabled` : ''}`);
290
+ log(`[adopt --status] scanned ${dirs.length} memdir(s): ${legacy} with legacy sentinel (await migration), ${disabled} auto-adopt-disabled.`);
291
+ if (legacy > 0) log('[adopt --status] run `claude-mem-lite adopt --all` to sweep legacy memory-dir sentinels now.');
298
292
 
299
- // Gating snapshot — helps debug "why didn't auto-adopt fire?"
300
293
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ? 'set' : 'unset';
301
294
  const noAutoAdopt = process.env.MEM_NO_AUTO_ADOPT === '1' ? '1 (opt-out)' : 'unset';
302
295
  log('');
303
- log('Auto-adopt gates (next SessionStart will fire only if both pass):');
304
- log(` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (plugin-mode install required; npx stays opt-in)`);
296
+ log('Auto-adopt gates (next SessionStart fires only if these pass):');
297
+ log(` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (any install path is consent; gate is the per-project opt-out below)`);
305
298
  log(` MEM_NO_AUTO_ADOPT = ${noAutoAdopt} (global escape hatch)`);
306
299
  log('Per-project opt-out: `claude-mem-lite adopt --disable` (run --enable to re-arm).');
307
300
  }
308
301
 
309
302
  /**
310
- * cmdUnadopt — precise removal of sentinel section + plugin doc.
311
- * Exit code stays 0: unadopt is idempotent; "absent" isn't an error.
312
- *
313
- * Flags:
314
- * --all Operate on every memdir under ~/.claude/projects/*\/memory/
315
- * --status Read-only: list currently-adopted memdirs (mirrors `adopt --status`).
316
- * --dry-run Preview what would be removed; no filesystem writes.
317
- *
318
- * Pre-fix history: unrecognized flags (e.g. `--status` extrapolated from `adopt --status`,
319
- * or `--dry-run` extrapolated from `adopt --dry-run`) were silently ignored and the
320
- * destructive default ran anyway, removing the sentinel block when the user expected
321
- * a read-only probe.
303
+ * cmdUnadopt — remove the CLAUDE.md managed block + detail doc for the current
304
+ * project, and clean any legacy memory-dir residue. `--all` sweeps the legacy
305
+ * memory-dir cleanup across every memdir (CLAUDE.md blocks for other projects
306
+ * can't be located from the lossy slug). Idempotent: exit code stays 0.
322
307
  */
323
308
  export function cmdUnadopt(args = []) {
324
309
  if (hasFlag(args, '--status')) return statusAll();
@@ -326,41 +311,21 @@ export function cmdUnadopt(args = []) {
326
311
  const all = hasFlag(args, '--all');
327
312
  const dryRun = hasFlag(args, '--dry-run');
328
313
  const force = hasFlag(args, '--force');
329
- const targets = all
330
- ? listAllMemdirs().map((m) => m.memdir)
331
- : [memdirPath(detectCwd())];
332
314
 
333
- if (targets.length === 0) {
334
- log('[unadopt] no memdirs found');
335
- return;
336
- }
315
+ if (all) return migrateAll(['--all', ...(force ? ['--force'] : []), ...(dryRun ? ['--dry-run'] : [])]);
337
316
 
338
- let removed = 0, absent = 0, skipped = 0;
339
- for (const memdir of targets) {
340
- if (dryRun) {
341
- // Mirror the live foreign-content guard: a sentinel with no state sidecar would be
342
- // skipped (not removed) unless --force, so dry-run must report it the same way.
343
- const action = !isAdopted(memdir, PLUGIN_SLUG) ? 'absent'
344
- : (hasPluginState(memdir, PLUGIN_SLUG) || force) ? 'would-remove'
345
- : 'would-skip-foreign';
346
- log(`[unadopt --dry-run] ${memdir} → ${action}`);
347
- if (action === 'would-remove') removed++;
348
- else if (action === 'would-skip-foreign') skipped++;
349
- else absent++;
350
- continue;
351
- }
352
- const r = removePluginSection(memdir, PLUGIN_SLUG, { force });
353
- if (r.action === 'removed') { removePluginDoc(memdir, PLUGIN_SLUG); removed++; }
354
- else if (r.action === 'skipped-foreign') skipped++;
355
- else absent++;
356
- log(`[unadopt] ${memdir} → ${r.action}`);
317
+ const cwd = detectCwd();
318
+ if (dryRun) {
319
+ const blockState = claudeMdIsAdopted(cwd, PLUGIN_SLUG) ? 'would-remove CLAUDE.md block + detail doc' : 'no CLAUDE.md block';
320
+ const legacy = hasLegacyMemdirSentinel(cwd, PLUGIN_SLUG) ? 'would-clean legacy memory-dir sentinel' : 'no legacy residue';
321
+ log(`[unadopt --dry-run] ${cwd}`);
322
+ log(` ${blockState}`);
323
+ log(` ${legacy}`);
324
+ return;
357
325
  }
358
326
 
359
- if (skipped > 0) {
360
- log('[unadopt] skipped-foreign = a sentinel block with no plugin state file (not proven plugin-written).');
361
- log('[unadopt] pass --force to remove it anyway.');
362
- }
363
- log('');
364
- const verb = dryRun ? 'would remove' : 'removed';
365
- log(`[unadopt${dryRun ? ' --dry-run' : ''}] ${targets.length} target(s): ${removed} ${verb}, ${skipped} skipped-foreign, ${absent} absent`);
327
+ const r = removeManaged(cwd, PLUGIN_SLUG);
328
+ const mig = migrateLegacyMemoryDir(cwd, PLUGIN_SLUG, { force });
329
+ const migNote = mig.action === 'removed' ? ' (+cleaned legacy memdir)' : '';
330
+ log(`[unadopt] ${cwd} → ${r.action}${migNote}`);
366
331
  }
package/adopt-content.mjs CHANGED
@@ -1,96 +1,139 @@
1
- // Phase C (Invited-Memory plan, T10): Content generators for the
2
- // claude-mem-lite sentinel line and its companion plugin_claude_mem_lite.md
3
- // detail doc. Kept separate so the strings are testable without side effects
4
- // and can evolve without touching memdir.mjs primitives.
1
+ // CLAUDE.md-steering plan (v3.13): content generators for the claude-mem-lite
2
+ // managed block (written into <cwd>/CLAUDE.md) and its companion
3
+ // <cwd>/.claude/plugin_claude_mem_lite.md detail doc. Kept separate from the
4
+ // claudemd.mjs primitives so the strings are testable without side effects.
5
5
  //
6
- // Bumping CURRENT_SENTINEL_VERSION: pick the next vN and update
7
- // docs/plans/2026-04-16-invited-memory-pattern.md so installs do a version-
8
- // bump replace instead of treating the old content as a user edit.
6
+ // Bumping CURRENT_SENTINEL_VERSION: pick the next vN. On the next SessionStart
7
+ // needsRefresh() sees the version change and refreshes the block in place
8
+ // (version bump = intended content change, so it overwrites rather than treating
9
+ // the drift as a user edit). v1 = legacy memory-dir sentinel (now migrated away);
10
+ // v2 = project-tree CLAUDE.md managed block.
9
11
 
10
12
  import { CLI_INVOKE } from './cli-path.mjs';
11
13
 
12
14
  export const PLUGIN_SLUG = 'claude-mem-lite';
13
- export const CURRENT_SENTINEL_VERSION = 'v1';
15
+ export const CURRENT_SENTINEL_VERSION = 'v2';
14
16
 
15
17
  /**
16
- * One-line entry injected into MEMORY.md's sentinel section. Must stay ≤150
17
- * chars so it clears Claude Code's 200-line MEMORY.md cap with headroom.
18
+ * The concise managed block injected into <cwd>/CLAUDE.md (between the
19
+ * slug-scoped sentinels those are added by claudemd.renderBlock, NOT here).
20
+ * Universal: the memory contract is the same for every project, so there is no
21
+ * per-project-type variation. Keep it tight (cheap always-loaded context); the
22
+ * full tables + rules live in the detail doc this block points to.
18
23
  */
19
- export function getIndexLine() {
20
- return '- [claude-mem-lite](plugin_claude_mem_lite.md) Edit `mem_recall(file=…)`;bugfix `mem_save(type="bugfix", lesson_learned, importance=2)`';
24
+ export function buildClaudeMdBlock() {
25
+ // Intentionally machine-stable: MCP tool names only, NO CLI_INVOKE (that
26
+ // resolves to an absolute path that differs per install — it would make this
27
+ // committed/refreshed block churn across machines). The robust CLI table lives
28
+ // in the detail doc (.claude/, gitignored).
29
+ return `## claude-mem-lite — 持久记忆 (persistent memory)
30
+
31
+ PreToolUse hooks 在 Read/Edit/Write 前已自动 \`mem_recall\` 过往教训。下面是值得你主动发起的调用:
32
+
33
+ | 时机 | 调用 |
34
+ |------|------|
35
+ | Edit/Write 前 | hook 已自动 recall;若注入了 \`#NN\` 教训,下次产出用户可见文字时引用 \`#NN\`(引用=采纳反馈,未引用会衰减) |
36
+ | 解决非平凡 bug 后 | \`mem_save(type="bugfix", lesson_learned="<根因+修法>", importance=2)\` |
37
+ | 非显然架构决策后 | \`mem_save(type="decision", lesson_learned="<约束+取舍>")\` |
38
+ | 推迟到下个会话 | \`mem_defer({title, priority:1|2|3, detail})\`;修好时给 \`mem_save\` 加 \`closes_deferred=[N]\` |
39
+ | 查过往工作 / 历史 | \`mem_search "关键词"\` · \`mem_recent\` · \`mem_timeline\` |
40
+
41
+ 完整工具+CLI 表、citation/decay 规则、save 纪律见 → \`.claude/plugin_claude_mem_lite.md\``;
21
42
  }
22
43
 
23
44
  /**
24
- * Full detail doc rendered into `memdir/plugin_claude_mem_lite.md`. Not
25
- * auto-loaded by Claude Code — the MEMORY.md index line points to it and
26
- * Claude reads it on demand when the injected hint suggests relevance.
45
+ * Full detail doc rendered into `<cwd>/.claude/plugin_claude_mem_lite.md`.
46
+ * Not auto-loaded by Claude Code — the CLAUDE.md block points to it and Claude
47
+ * reads it on demand. claudemd.writeManaged() prepends the `managed-by` marker;
48
+ * this returns pure content.
27
49
  */
28
50
  export function getDetailDoc() {
29
- return `# claude-mem-lite 插件契约
51
+ return `# claude-mem-lite 插件契约(完整)
52
+
53
+ > 由 \`${CLI_INVOKE} adopt\` 生成、随版本自动刷新;卸载用 \`${CLI_INVOKE} unadopt\`。
54
+ > 精炼触发表在项目 \`CLAUDE.md\` 的 \`claude-mem-lite\` 托管块里;本文件是其展开。
55
+ > 设计背景见 docs/CLAUDE-MD-STEERING-PLAN.md。
30
56
 
31
- > \`${CLI_INVOKE} adopt\` 生成;卸载用 \`${CLI_INVOKE} unadopt\`。
32
- > 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
57
+ ## 被动 recall(hook 已自动跑,你只需采纳)
33
58
 
34
- ## 何时调用 MCP 工具
59
+ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该文件:
60
+ - **Read** 路径:asymmetric-quiet——最多 1 条 lesson、120 字符、要求带 \`lesson_learned\`。
61
+ - **Edit / Write** 路径:decision-support——最多 3 条、240 字符、高重要度 bugfix/decision 即使无
62
+ lesson 也注入。
63
+ - Read→Edit 同文件共享 cooldown(不重复注入正文),但 Read 注入后的首个 Edit 会把 lesson **ID**
64
+ 以一行 ack 指令重新浮出。看到 \`#NN [bugfix] …\` 这类行时:**下次产出用户可见文字时引用 \`#NN\`**
65
+ (\`'#NN applied'\` 或 \`'#NN n/a — <理由>'\`)。纯工具回合不算;把 ID 记在工作记忆里,写回时引用。
66
+ - 系统按会话追踪引用:未引用的 lesson 连续 3 个会话后 importance −1(地板 0),被引用的 +1(封顶 3)。
67
+ 引用是给系统的反馈,不是合规仪式——注入池据此自调。
35
68
 
36
- 以下 6 个核心 MCP 工具在 \`tools/list\` 中默认暴露,覆盖了契约的热路径:
37
- \`mem_search\` / \`mem_recent\` / \`mem_recall\` / \`mem_get\` / \`mem_save\` / \`mem_timeline\`。
69
+ ## 何时主动调用 MCP 工具
70
+
71
+ \`tools/list\` 默认暴露 6 个核心工具 + 3 个 defer 工具:
72
+ \`mem_search\` / \`mem_recent\` / \`mem_recall\` / \`mem_get\` / \`mem_save\` / \`mem_timeline\` +
73
+ \`mem_defer\` / \`mem_defer_list\` / \`mem_defer_drop\`。
38
74
 
39
75
  | 时机 | 工具 | 关键参数 |
40
76
  |------|------|----------|
41
- | Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\` —— 过往 bugfix 与教训 |
77
+ | Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\`(hook 通常已代劳) |
42
78
  | Test failure / error | \`mem_search\` | \`query="<错误关键词>", obs_type="bugfix"\` |
43
79
  | Refactor 前 | \`mem_search\` | \`query="<模块>", obs_type="refactor"\` |
44
80
  | 新功能起手 | \`mem_search\` | \`query="<功能区域>"\` —— 找 prior art |
45
- | Bugfix 解决后 | \`mem_save\` | \`type="bugfix", lesson_learned="<根因+修法>", importance=2\` |
46
- | 架构决策后 | \`mem_save\` | \`type="decision", lesson_learned="<取舍理由>", importance=2\` |
81
+ | 解决非平凡 bug | \`mem_save\` | \`type="bugfix", lesson_learned="<根因+修法>", importance=2\` |
82
+ | 非显然架构决策后 | \`mem_save\` | \`type="decision", lesson_learned="<约束+取舍>"\` |
47
83
  | 上下文提到 #NN | \`mem_get\` | \`ids=[NN]\` |
48
84
 
49
- ## Decision rules(替代多步 search)
50
-
51
- - "最近做了啥" \`mem_recent\`
52
- - "<文件> 有哪些记忆" → \`mem_recall\`
53
- - "#NN 前后发生了啥" \`mem_timeline\`
85
+ ## 必做契约(dogfood,本仓库尤其严格)
86
+
87
+ - **解决非平凡 bug 后**(≠ typo / rename)**必须** \`mem_save(type="bugfix",
88
+ lesson_learned="<一行根因+一行修法>", importance=2)\`。判据:未来改同一文件的会话看到这条能否避坑?能→存。
89
+ - **非显然架构决策后**(≠ 改名/挪代码)调 \`mem_save(type="decision",
90
+ lesson_learned="<约束+为何这样选+牺牲了什么>")\`。\`decision\` 命中率显著高于 \`change\`(当前遥测约
91
+ 3:1,会漂移——用 \`${CLI_INVOKE} stats\` 实测,别套固定倍数);方向稳健:一条好 decision 抵数条 change。
92
+ 别注水:decision 只留给真权衡,不是风格选择。
93
+ - **推迟到未来会话**(≠ 在途 todo、≠ 本 PR 跟进)调
94
+ \`mem_defer({title, priority:1|2|3, detail:"<约束+为何推迟>"})\`。
95
+ 触发词:中文「下次/下个会话/不在本轮范围/留给下个会话」;en「next session / defer to next round /
96
+ out of scope for this PR / pick up later」。
97
+ - 修掉 deferred 项时 **必须** 给 \`mem_save\` 加 \`closes_deferred=[N]\`(N 是 SessionStart
98
+ \`### Deferred Work\` banner 里的序号,或原始 id \`["D#42"]\`,混用 OK),让 carry-forward 链闭合。
99
+ 若该项无需修(flaky/scope shift)改用 \`mem_defer_drop({id, reason})\`,reason 必填、作审计。
100
+ - **不要为凑 schema 写 \`lesson_learned: 'none'\`**:写不出能复用的教训就留 NULL,接受低重要度观测。
101
+ Haiku 默认过于激进地填 "none"——手动 save 时覆盖它。
54
102
 
55
103
  ## 维护 / 管理类工具(走 CLI)
56
104
 
57
- v2.34.0 起,以下 11 个工具从 \`tools/list\` 中隐藏以缩小启动上下文;它们仍注册在
58
- MCP 层,按名 \`tools/call\` 仍可命中,但对 Claude Code 这类只读 tools/list
59
- 调用方只走下面的 CLI 入口:
105
+ 以下工具从 \`tools/list\` 隐藏(缩小启动上下文);仍注册在 MCP 层、按名 \`tools/call\` 可命中,
106
+ 但对 Claude Code 这类只读 tools/list 的调用方只走 CLI:
60
107
 
61
108
  | 场景 | CLI |
62
109
  |------|-----|
63
- | 清理过期记忆 | \`${CLI_INVOKE} maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale --confirm\`(execute 删行必须带 \`--confirm\`,否则只预览) |
64
- | 深度优化(Haiku) | \`${CLI_INVOKE} optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\` 选阶段) |
65
- | 压缩旧条目 | \`${CLI_INVOKE} compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\` 改阈值) |
110
+ | 清理过期记忆 | \`${CLI_INVOKE} maintain scan --ops purge_stale\` → \`maintain execute --ops purge_stale --confirm\`(删行必须 \`--confirm\`) |
111
+ | 深度优化(Haiku) | \`${CLI_INVOKE} optimize\`(默认 preview;\`--run\` 执行,\`--task re-enrich,normalize,cluster-merge,smart-compress\`) |
112
+ | 压缩旧条目 | \`${CLI_INVOKE} compress\`(默认 preview;\`--execute\` 执行,\`--age-days N\`) |
66
113
  | FTS5 索引检查 / 重建 | \`${CLI_INVOKE} fts-check [--rebuild]\` |
67
114
  | tier 分组浏览 | \`${CLI_INVOKE} browse [--tier active]\` |
68
115
  | 导出 JSON/JSONL | \`${CLI_INVOKE} export [--format jsonl]\` |
69
116
  | 统计总量 / 健康 | \`${CLI_INVOKE} stats [--days 30]\` |
70
- | 删除某条 | \`${CLI_INVOKE} delete <id>[,<id>]\` |
71
- | 更新某条 | \`${CLI_INVOKE} update <id> [--title ...]\` |
72
- | 列 / 搜索 / 导入 skill-agent registry | \`${CLI_INVOKE} registry <list\\|search\\|import>\` |
73
- | 按 registry 名载入 skill/agent | (MCP only:\`mem_use\`;由用户主动请求时才使用) |
117
+ | 删除 / 更新某条 | \`${CLI_INVOKE} delete <id>[,<id>]\` · \`${CLI_INVOKE} update <id> [--title ...]\` |
118
+ | skill-agent registry | \`${CLI_INVOKE} registry <list\\|search\\|import>\` |
74
119
 
75
120
  ## CLI 速查(常用检索)
76
121
 
77
122
  | 命令 | 用途 |
78
123
  |------|------|
79
- | \`${CLI_INVOKE} search "query"\` | FTS5 全文搜索 |
124
+ | \`${CLI_INVOKE} search "query"\` | FTS5 全文搜索(默认排除低信号 \`Modified X\` 等;加 \`--include-noise\` 找文件变更记录) |
80
125
  | \`${CLI_INVOKE} search "err" --type bugfix\` | 按类型过滤 |
81
126
  | \`${CLI_INVOKE} recall "file.mjs"\` | 文件相关记忆 |
82
127
  | \`${CLI_INVOKE} recent 5\` | 最近 5 条 |
83
128
  | \`${CLI_INVOKE} get 42,43\` | 按 ID 展开 |
84
129
  | \`${CLI_INVOKE} timeline --anchor 42\` | 时间线上下文 |
85
130
 
86
- ## 质量门槛
87
-
88
- - \`mem_save\` 的 \`lesson_learned\` 不要写 \`none\`——写不出教训就保持 NULL
89
- - \`decision\` 的命中率高于 \`change\`(当前遥测约 3:1,数值会漂移——用 \`${CLI_INVOKE} stats\` 实测,别套固定倍数);方向稳健:一条好 decision 抵数条 change
90
- - 一般搜索跳过 \`obs_type\` 让系统自动路由;特定意图再过滤
91
-
92
- ## 卸载
131
+ ## 卸载 / 关闭
93
132
 
94
- \`${CLI_INVOKE} unadopt\` 精确移除 sentinel + 本文件;其它 MEMORY.md 内容不动。
133
+ - \`${CLI_INVOKE} unadopt\`:移除 CLAUDE.md 托管块 + \`.claude/plugin_claude_mem_lite.md\`;
134
+ CLAUDE.md 里你自己的内容(sentinel 之外)不动。
135
+ - 本项目永久关闭自动 adopt:\`${CLI_INVOKE} adopt --disable\`(\`--enable\` 重新武装)。
136
+ - 全局禁用自动 adopt:环境变量 \`MEM_NO_AUTO_ADOPT=1\`。
137
+ - 关闭版本漂移自动刷新(保留你对托管块的手改):\`CLAUDE_MEM_NO_TEMPLATE_REFRESH=1\`。
95
138
  `;
96
139
  }
package/claudemd.mjs ADDED
@@ -0,0 +1,244 @@
1
+ // CLAUDE.md-steering plan (v3.13): claudemd.mjs — primitives for the
2
+ // project-tree managed block at <cwd>/CLAUDE.md plus an on-demand detail doc
3
+ // at <cwd>/.claude/plugin_<slug>.md.
4
+ //
5
+ // Why here and not memdir.mjs: Claude Code loads the project CLAUDE.md and the
6
+ // memory-dir MEMORY.md at EQUAL weight, so steering belongs in CLAUDE.md (the
7
+ // canonical home for project instructions) — seeding MEMORY.md just pollutes an
8
+ // index meant for the user's own memories. This module mirrors the design
9
+ // shipped by the sibling code-graph-mcp plugin (claude-plugin/scripts/adopt.js)
10
+ // and the oh-my-claudecode versioned `<!-- :begin vN -->` managed-block pattern.
11
+ //
12
+ // The managed block is plugin-owned: it auto-refreshes when the shipped content
13
+ // drifts (version bump or template change), UNLESS CLAUDE_MEM_NO_TEMPLATE_REFRESH=1.
14
+ // User prose OUTSIDE the slug-scoped sentinel is never touched. The slug scope is
15
+ // what keeps this independent from code-graph-mcp's own block in the same file.
16
+ //
17
+ // See docs/CLAUDE-MD-STEERING-PLAN.md for rationale + migration.
18
+
19
+ import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, mkdirSync, rmdirSync, readdirSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { createHash } from 'crypto';
22
+ import { memdirPath, removePluginSection, removePluginDoc, isAdopted as memdirIsAdopted } from './memdir.mjs';
23
+
24
+ // ─── Path helpers ────────────────────────────────────────────────────────────
25
+
26
+ function slugSnake(slug) { return String(slug).replace(/[^a-zA-Z0-9]/g, '_'); }
27
+
28
+ export function claudeMdPath(cwd) { return join(cwd, 'CLAUDE.md'); }
29
+ function dotClaudeDir(cwd) { return join(cwd, '.claude'); }
30
+ export function detailDocPath(cwd, slug) { return join(dotClaudeDir(cwd), `plugin_${slugSnake(slug)}.md`); }
31
+ function stateFilePath(cwd, slug) { return join(dotClaudeDir(cwd), `.plugin_${slugSnake(slug)}_state.json`); }
32
+
33
+ // First line of the detail doc — an invisible (in rendered markdown) marker that
34
+ // lets us distinguish our generated copy from a user's same-named file.
35
+ function managedByMarker(slug) { return `<!-- managed-by: ${slug} -->`; }
36
+
37
+ // ─── Sentinel rendering & parsing ────────────────────────────────────────────
38
+
39
+ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
40
+
41
+ // Slug-scoped so stripping our block never disturbs another plugin's block
42
+ // (e.g. code-graph-mcp's `<!-- code-graph-mcp:begin -->`) sitting in the same
43
+ // CLAUDE.md. Matches any version vN+ for migration/idempotency. The separators
44
+ // are `\r?\n` (not bare `\n`) so a CLAUDE.md re-saved with Windows CRLF endings
45
+ // still matches — otherwise the block read as "absent" and a fresh LF copy got
46
+ // appended every SessionStart, growing the file without bound (review C1/H2).
47
+ function blockBody(esc) { return `<!-- ${esc}:begin (v\\d+) -->\\r?\\n([\\s\\S]*?)\\r?\\n<!-- ${esc}:end -->`; }
48
+ function blockRegex(slug) { return new RegExp(blockBody(escapeRe(slug))); }
49
+ function blockRegexG(slug) { return new RegExp(blockBody(escapeRe(slug)), 'g'); }
50
+
51
+ function renderBlock(slug, version, body) {
52
+ return `<!-- ${slug}:begin ${version} -->\n${body}\n<!-- ${slug}:end -->`;
53
+ }
54
+
55
+ function sha256(s) { return createHash('sha256').update(s).digest('hex'); }
56
+
57
+ function atomicWrite(path, content) {
58
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
59
+ writeFileSync(tmp, content);
60
+ renameSync(tmp, path);
61
+ }
62
+
63
+ function writeState(cwd, slug, state) {
64
+ const dir = dotClaudeDir(cwd);
65
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
66
+ atomicWrite(stateFilePath(cwd, slug), JSON.stringify(state, null, 2) + '\n');
67
+ }
68
+
69
+ function clearState(cwd, slug) {
70
+ const p = stateFilePath(cwd, slug);
71
+ if (existsSync(p)) try { unlinkSync(p); } catch { /* best-effort */ }
72
+ }
73
+
74
+ // ─── Public API ──────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Parse <cwd>/CLAUDE.md for our slug-scoped managed block.
78
+ * @returns {{ exists: boolean, version: string|null, body: string|null, raw: string }}
79
+ */
80
+ export function readBlock(cwd, slug) {
81
+ const p = claudeMdPath(cwd);
82
+ if (!existsSync(p)) return { exists: false, version: null, body: null, raw: '' };
83
+ const raw = readFileSync(p, 'utf8');
84
+ const m = raw.match(blockRegex(slug));
85
+ if (!m) return { exists: true, version: null, body: null, raw };
86
+ // Normalize CRLF in the captured body so drift comparison (needsRefresh) is
87
+ // line-ending-agnostic — a CRLF-saved file matching the shipped LF content
88
+ // must NOT be seen as drifted and rewritten every session.
89
+ return { exists: true, version: m[1], body: m[2].replace(/\r\n/g, '\n'), raw };
90
+ }
91
+
92
+ /**
93
+ * Adopted under the new scheme = managed block present in CLAUDE.md AND the
94
+ * detail doc exists. Both must hold so a half-written state still self-heals.
95
+ */
96
+ export function isAdopted(cwd, slug) {
97
+ const blk = readBlock(cwd, slug);
98
+ return blk.body !== null && existsSync(detailDocPath(cwd, slug));
99
+ }
100
+
101
+ /**
102
+ * Whether the installed block/doc has drifted from the shipped content — i.e.
103
+ * a version bump or a template edit means we should refresh. Returns true when
104
+ * the block is missing, the version differs, the block body differs, or the
105
+ * detail doc is missing / differs. The block is plugin-managed: drift is
106
+ * overwritten on refresh (opt out with CLAUDE_MEM_NO_TEMPLATE_REFRESH=1 at the
107
+ * caller). User content lives OUTSIDE the sentinel and is never compared.
108
+ */
109
+ export function needsRefresh(cwd, { slug, version, block, doc }) {
110
+ const blk = readBlock(cwd, slug);
111
+ if (blk.body === null) return true;
112
+ if (blk.version !== version) return true;
113
+ if (blk.body !== block) return true;
114
+ const dp = detailDocPath(cwd, slug);
115
+ if (!existsSync(dp)) return true;
116
+ let cur;
117
+ try { cur = readFileSync(dp, 'utf8'); } catch { return true; }
118
+ return cur !== `${managedByMarker(slug)}\n${doc}`;
119
+ }
120
+
121
+ /**
122
+ * Write (insert-or-replace) the managed block in CLAUDE.md and the detail doc
123
+ * under .claude/, plus a state sidecar. Idempotent on the user-visible files:
124
+ * re-running with identical inputs leaves CLAUDE.md and the detail doc byte-for-
125
+ * byte unchanged (only the state sidecar's writtenAt updates).
126
+ *
127
+ * CLAUDE.md is created if absent. Only our slug-scoped region is rewritten;
128
+ * everything else in the file is preserved verbatim.
129
+ *
130
+ * @returns {{action: 'created'|'updated'|'unchanged'}} (block disposition)
131
+ */
132
+ export function writeManaged(cwd, { slug, version, block, doc }) {
133
+ const p = claudeMdPath(cwd);
134
+ const raw = existsSync(p) ? readFileSync(p, 'utf8') : '';
135
+ const section = renderBlock(slug, version, block);
136
+ const m = raw.match(blockRegex(slug));
137
+
138
+ let next, action;
139
+ if (!m) {
140
+ if (raw.length === 0) next = section + '\n';
141
+ else if (raw.endsWith('\n\n')) next = raw + section + '\n';
142
+ else if (raw.endsWith('\n')) next = raw + '\n' + section + '\n';
143
+ else next = raw + '\n\n' + section + '\n';
144
+ action = 'created';
145
+ } else {
146
+ next = raw.replace(m[0], section);
147
+ action = next !== raw ? 'updated' : 'unchanged';
148
+ }
149
+ // H2: collapse any DUPLICATE same-slug blocks (keep the first, drop the rest).
150
+ // Defends against a CRLF-orphaned copy a pre-fix build may have appended, or a
151
+ // user paste — otherwise the extras would be invisible to refresh/unadopt.
152
+ let seen = 0;
153
+ const deduped = next.replace(blockRegexG(slug), (whole) => (seen++ === 0 ? whole : ''));
154
+ if (deduped !== next) {
155
+ next = deduped.replace(/\n{3,}/g, '\n\n');
156
+ if (action === 'unchanged') action = 'updated';
157
+ }
158
+ if (next !== raw) atomicWrite(p, next);
159
+
160
+ // Detail doc (marker on first line so unadopt/refresh can tell it apart from a
161
+ // user's same-named file).
162
+ const docContent = `${managedByMarker(slug)}\n${doc}`;
163
+ const dp = detailDocPath(cwd, slug);
164
+ const dir = dotClaudeDir(cwd);
165
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
166
+ const existingDoc = existsSync(dp) ? readFileSync(dp, 'utf8') : null;
167
+ if (existingDoc !== docContent) atomicWrite(dp, docContent);
168
+
169
+ writeState(cwd, slug, {
170
+ version,
171
+ blockHash: sha256(block),
172
+ docHash: sha256(doc),
173
+ writtenAt: new Date().toISOString(),
174
+ });
175
+ return { action };
176
+ }
177
+
178
+ /**
179
+ * Remove our managed block from CLAUDE.md (preserving all other content) and
180
+ * delete the detail doc + state sidecar. Best-effort removes an emptied
181
+ * .claude/ directory.
182
+ * @returns {{action: 'removed'|'absent'}}
183
+ */
184
+ export function removeManaged(cwd, slug) {
185
+ const p = claudeMdPath(cwd);
186
+ let action = 'absent';
187
+ if (existsSync(p)) {
188
+ let raw = readFileSync(p, 'utf8');
189
+ // H2: loop so ALL same-slug blocks are removed, not just the first (a
190
+ // duplicate/CRLF-orphaned copy must not survive unadopt).
191
+ let m;
192
+ while ((m = raw.match(blockRegex(slug)))) {
193
+ const blockAtStart = m.index === 0;
194
+ let start = m.index;
195
+ let end = m.index + m[0].length;
196
+ if (raw[end] === '\n') end++;
197
+ if (start > 0 && raw.slice(0, start).endsWith('\n\n')) start--;
198
+ raw = raw.slice(0, start) + raw.slice(end);
199
+ raw = raw.replace(/\n{3,}/g, '\n\n');
200
+ if (blockAtStart) raw = raw.replace(/^\s+/, '');
201
+ action = 'removed';
202
+ }
203
+ if (action === 'removed') atomicWrite(p, raw);
204
+ }
205
+ const dp = detailDocPath(cwd, slug);
206
+ if (existsSync(dp)) try { unlinkSync(dp); } catch { /* best-effort */ }
207
+ clearState(cwd, slug);
208
+ // Drop an emptied .claude/ so unadopt leaves no trace (skips if it holds
209
+ // anything else — e.g. settings.local.json).
210
+ try {
211
+ const dir = dotClaudeDir(cwd);
212
+ if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
213
+ } catch { /* best-effort */ }
214
+ return { action };
215
+ }
216
+
217
+ // ─── Legacy migration ────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * One-time (idempotent) cleanup of the pre-v3.13 scheme: strip the slug-scoped
221
+ * sentinel from the project's memory-dir MEMORY.md and delete the memory-dir
222
+ * detail doc + state sidecar. Slug-scoped, so an adjacent code-graph-mcp block
223
+ * and all user prose survive byte-intact. Respects the foreign-content guard
224
+ * (a sentinel with no state sidecar is left in place) unless force=true.
225
+ *
226
+ * Absent legacy artifacts → harmless no-op, so this is safe to call every
227
+ * SessionStart.
228
+ *
229
+ * @returns {{action: 'removed'|'absent'|'skipped-foreign'}}
230
+ */
231
+ export function migrateLegacyMemoryDir(cwd, slug, { force = false } = {}) {
232
+ const memdir = memdirPath(cwd);
233
+ const r = removePluginSection(memdir, slug, { force });
234
+ // M3: only delete the legacy detail doc when we actually removed OUR sentinel.
235
+ // On 'skipped-foreign' the sentinel is left in place (not provably plugin-
236
+ // written), so deleting its companion doc would leave a dangling pointer.
237
+ if (r.action === 'removed') removePluginDoc(memdir, slug);
238
+ return r;
239
+ }
240
+
241
+ /** True if the legacy memory-dir sentinel still exists for this project. */
242
+ export function hasLegacyMemdirSentinel(cwd, slug) {
243
+ return memdirIsAdopted(memdirPath(cwd), slug);
244
+ }
package/commands/adopt.md CHANGED
@@ -1,60 +1,67 @@
1
1
  ---
2
2
  name: adopt
3
- description: "Use when: user asks to increase claude-mem-lite's tool-invocation rate in the current project, or wants to install the invited-memory sentinel so Claude Code auto-loads the contract as user-memory. Writes a single sentinel-wrapped line to ~/.claude/projects/<encoded>/memory/MEMORY.md plus a plugin_claude_mem_lite.md detail file. Run /unadopt to remove."
3
+ description: "Use when: user asks to increase claude-mem-lite's tool-invocation rate in the current project, or to (re)install the steering block. Writes a sentinel-wrapped managed block into <cwd>/CLAUDE.md plus a <cwd>/.claude/plugin_claude_mem_lite.md detail doc, and migrates away any legacy memory-dir sentinel. Runs automatically on SessionStart; use this to force it now. Run /unadopt to remove."
4
4
  ---
5
5
 
6
6
  # /adopt
7
7
 
8
- Install the **invited-memory** sentinel into the current project's Claude Code
9
- memdir (`~/.claude/projects/<encoded>/memory/`). The sentinel line carries the
10
- MCP-tool triggers (`mem_recall` / `mem_save`) at system-prompt authority
11
- (framed as "user's auto-memory") so Claude Code is more likely to call them
12
- proactively than when those same triggers live in MCP server instructions.
8
+ Install the claude-mem-lite **steering block** into the current project's
9
+ `CLAUDE.md` (the canonical home for project instructions — loaded by Claude Code
10
+ at system-prompt authority). This replaces the pre-v3.13 scheme that seeded the
11
+ project's memory-dir `MEMORY.md`; that polluted an index meant for the user's own
12
+ memories, and `MEMORY.md` carries no more weight than `CLAUDE.md` anyway.
13
+
14
+ This normally runs automatically on every SessionStart — invoke `/adopt` only to
15
+ force it immediately (e.g. after editing the block out by hand).
13
16
 
14
17
  ## What it writes
15
18
 
16
- 1. **`MEMORY.md`** — ONE sentinel-wrapped line under a `## 插件契约` header.
17
- Idempotent: re-running replaces the block only if the version bumped.
18
- 2. **`plugin_claude_mem_lite.md`** Detailed contract (CLI cheatsheet +
19
- MCP decision rules). Not auto-loaded; Claude reads it on demand when the
20
- MEMORY.md pointer surfaces in context.
21
- 3. **`.plugin_claude_mem_lite_state.json`** Sidecar with sentinel body hash
22
- for user-edit detection.
19
+ 1. **`<cwd>/CLAUDE.md`** — a concise `<!-- claude-mem-lite:begin v2 -->…<!-- :end -->`
20
+ managed block (trigger table `mem_recall` / `mem_save` / `mem_defer`).
21
+ Slug-scoped: only this block is managed; the rest of your `CLAUDE.md` is
22
+ preserved verbatim. Auto-refreshes when the shipped content drifts.
23
+ 2. **`<cwd>/.claude/plugin_claude_mem_lite.md`** the full contract (tool tables,
24
+ CLI cheatsheet, citation/decay + save discipline). First line is a
25
+ `<!-- managed-by: claude-mem-lite -->` marker. Not auto-loaded; the CLAUDE.md
26
+ block points to it and Claude reads it on demand.
27
+ 3. **`<cwd>/.claude/.plugin_claude_mem_lite_state.json`** — drift-tracking sidecar.
28
+
29
+ It also **migrates** this project's legacy memory-dir sentinel away (strips the
30
+ `claude-mem-lite:*` block from `MEMORY.md` and deletes the old memory-dir detail
31
+ doc). Other plugins' blocks (e.g. `code-graph-mcp:*`) and your own prose survive.
23
32
 
24
33
  ## Flags
25
34
 
26
- - `--force` — overwrite a sentinel block that was manually edited
35
+ - `--force` — also force-clean a legacy memory-dir block lacking a state sidecar
27
36
  - `--dry-run` — print intended writes without touching disk
28
- - `--all` — adopt every memdir under `~/.claude/projects/*/memory/`
29
- - `--status` list all adopted projects with their sentinel versions
37
+ - `--all` — legacy-cleanup sweep: strip the old memory-dir sentinel across **every**
38
+ project at once. (It does NOT write CLAUDE.md blocks for other projects their
39
+ real paths can't be recovered from the encoded memdir slug; those adopt
40
+ per-project on each one's next SessionStart.)
41
+ - `--status` — show this project's adoption + count of memdirs awaiting migration
42
+ - `--disable` / `--enable` — per-project opt-out of automatic SessionStart adopt
30
43
 
31
- ## Removal
44
+ ## Removal & opt-out
32
45
 
33
- `/unadopt` precisely removes the sentinel block + plugin doc + state file.
34
- User content outside the sentinel is preserved.
46
+ - `/unadopt` removes the CLAUDE.md block + `.claude/` detail doc (your prose stays).
47
+ - `claude-mem-lite adopt --disable` permanently stops auto-adopt for this project.
48
+ - `MEM_NO_AUTO_ADOPT=1` disables auto-adopt globally.
49
+ - `CLAUDE_MEM_NO_TEMPLATE_REFRESH=1` freezes the block against drift-refresh
50
+ (keeps your hand-edits to the managed block).
35
51
 
36
52
  ## Conservative layer still active
37
53
 
38
54
  Adoption does NOT remove the hook-based injection (SessionStart context,
39
- UserPromptSubmit related-memory). Those remain as fallback for older Claude
40
- Code versions. Post-adopt the MCP `WHEN TO USE` section + the `File Lessons` /
41
- `Key Context` sections auto-trim since the sentinel line already carries the
42
- triggers at higher authority.
43
-
44
- ## Restart required for MCP trim to take effect
45
-
46
- The MCP server builds its instructions once at server boot and the protocol
47
- has no way to push updated instructions to an already-connected Claude Code
48
- session. After running `adopt`:
55
+ UserPromptSubmit related-memory). Those remain as fallback. Post-adopt the MCP
56
+ `WHEN TO USE` section + the `File Lessons` / `Key Context` sections auto-trim,
57
+ since the CLAUDE.md block already carries the triggers at higher authority.
49
58
 
50
- - The **MEMORY.md sentinel** appears on the **next SessionStart** automatically.
51
- - Hook-layer trim (`File Lessons` / `Key Context` / lesson suffix) applies
52
- on the next SessionStart.
53
- - MCP-instructions trim (`WHEN TO USE` / `Decision rules` sections) only
54
- takes effect after Claude Code itself restarts (or at least re-attaches
55
- the mem-lite MCP server). If you still see the verbose MCP instructions after
56
- adopt, a `/exit` + fresh session is enough.
59
+ ## Restart caveat for the MCP-instructions trim
57
60
 
61
+ The CLAUDE.md block + hook-layer trim apply on the **next SessionStart**. The MCP
62
+ server builds its instructions once at boot, so the MCP-instructions trim
63
+ (`WHEN TO USE` / `Decision rules`) only takes effect after Claude Code restarts
64
+ (or re-attaches the mem-lite MCP server). A `/exit` + fresh session is enough.
58
65
  Same caveat applies in reverse for `/unadopt`.
59
66
 
60
67
  !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs adopt $ARGUMENTS
@@ -1,30 +1,44 @@
1
1
  ---
2
2
  name: unadopt
3
- description: "Use when: user wants to remove the invited-memory sentinel from the current project (or all projects with --all). Cleans MEMORY.md sentinel block + plugin_claude_mem_lite.md + state sidecar. User content outside the sentinel is preserved. Benign no-op on never-adopted memdirs."
3
+ description: "Use when: user wants to remove the claude-mem-lite steering block from the current project (or sweep legacy memory-dir sentinels everywhere with --all). Removes the <cwd>/CLAUDE.md managed block + <cwd>/.claude/plugin_claude_mem_lite.md and cleans any legacy memory-dir residue. User content outside the sentinel is preserved. Benign no-op when not adopted."
4
4
  ---
5
5
 
6
6
  # /unadopt
7
7
 
8
- Remove the claude-mem-lite invited-memory sentinel from the current project's
9
- Claude Code memdir. Opposite of `/adopt`.
8
+ Remove the claude-mem-lite steering block from the current project. Opposite of
9
+ `/adopt`.
10
10
 
11
11
  ## What it removes
12
12
 
13
- 1. The `<!-- claude-mem-lite:begin vN --> ... <!-- claude-mem-lite:end -->`
14
- block from `MEMORY.md` (plus the preceding `## 插件契约` header line it owns).
15
- 2. `plugin_claude_mem_lite.md` detail file.
16
- 3. `.plugin_claude_mem_lite_state.json` sidecar.
13
+ 1. The `<!-- claude-mem-lite:begin vN --> <!-- claude-mem-lite:end -->` managed
14
+ block from `<cwd>/CLAUDE.md`. Everything else in the file is preserved
15
+ byte-for-byte.
16
+ 2. `<cwd>/.claude/plugin_claude_mem_lite.md` detail doc.
17
+ 3. `<cwd>/.claude/.plugin_claude_mem_lite_state.json` sidecar (and an emptied
18
+ `.claude/` dir).
17
19
 
18
- Everything else in `MEMORY.md` is preserved byte-for-byte.
20
+ It also cleans any leftover **legacy** memory-dir sentinel + detail doc for this
21
+ project (slug-scoped — other plugins' blocks survive).
19
22
 
20
23
  ## Flags
21
24
 
22
- - `--all` — unadopt every memdir under `~/.claude/projects/*/memory/`
25
+ - `--force` — also remove a legacy memory-dir block lacking a state sidecar
26
+ - `--dry-run` — preview what would be removed; no writes
27
+ - `--all` — legacy-cleanup sweep: strip the old memory-dir sentinel across every
28
+ project (CLAUDE.md blocks for other projects can't be located from the encoded
29
+ slug — `cd` into a project and run `/unadopt` there to remove its block).
30
+ - `--status` — read-only adoption probe (mirrors `/adopt --status`)
31
+
32
+ ## Note: this does not stop auto-adopt
33
+
34
+ `/unadopt` removes the block now, but the next SessionStart re-adopts unless you
35
+ also disable it: `claude-mem-lite adopt --disable` (per-project) or
36
+ `MEM_NO_AUTO_ADOPT=1` (global).
23
37
 
24
38
  ## Aftermath
25
39
 
26
40
  Once unadopted, the conservative hook layer (SessionStart `File Lessons` /
27
- `Key Context`, MCP instructions `WHEN TO USE`) goes back to verbose mode
28
- Claude Code will see the full injection again on the next session start.
41
+ `Key Context`, MCP instructions `WHEN TO USE`) returns to verbose mode on the
42
+ next session start.
29
43
 
30
44
  !node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS
package/hook-shared.mjs CHANGED
@@ -8,9 +8,11 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, readdir
8
8
  import { inferProject, debugCatch } from './utils.mjs';
9
9
  import { ensureDb, DB_DIR } from './schema.mjs';
10
10
  import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI, detectMode as detectLLMMode, callHaiku } from './haiku-client.mjs';
11
- // Phase D: invited-memory sentinel detection. memdir.mjs only pulls in fs/path/os/crypto;
12
- // adopt-content.mjs is pure strings. No circular deps — memdir doesn't import hook-shared.
13
- import { memdirPath as _memdirPath, isAdopted as _isAdopted } from './memdir.mjs';
11
+ // Phase D: invited-memory sentinel detection. memdir.mjs/claudemd.mjs only pull in
12
+ // fs/path/os/crypto; adopt-content.mjs is pure strings. No circular deps —
13
+ // neither imports hook-shared.
14
+ import { memdirPath as _memdirPath, isAdopted as _isAdoptedMemdir } from './memdir.mjs';
15
+ import { isAdopted as _isAdoptedClaudeMd } from './claudemd.mjs';
14
16
  import { PLUGIN_SLUG as _PLUGIN_SLUG } from './adopt-content.mjs';
15
17
 
16
18
  // ─── Constants ────────────────────────────────────────────────────────────────
@@ -38,14 +40,18 @@ export function isQuietHooks() {
38
40
  return process.env.MEM_QUIET_HOOKS === '1';
39
41
  }
40
42
 
41
- // Phase D (v2.32.1+): if the current project has adopted our invited-memory
42
- // sentinel, MEMORY.md already carries the triggers at system-prompt authority
43
- // — so hook + MCP-instruction output can also go quiet. isQuietHooks (env)
43
+ // Phase D (v2.32.1+) → v3.13: if the current project has adopted our steering,
44
+ // the contract is already loaded at system-prompt authority — so hook +
45
+ // MCP-instruction output can also go quiet. v3.13 moved that contract from the
46
+ // memory-dir MEMORY.md sentinel to the project CLAUDE.md managed block, so check
47
+ // the new scheme first and keep the legacy memdir sentinel as a fallback (an
48
+ // un-migrated project stays quiet through the transition). isQuietHooks (env)
44
49
  // remains an independent, stronger override.
45
50
  export function isAdoptedHere(cwd) {
46
51
  try {
47
52
  const resolved = cwd || process.env.CLAUDE_PROJECT_DIR || process.env.PWD || process.cwd();
48
- return _isAdopted(_memdirPath(resolved), _PLUGIN_SLUG);
53
+ return _isAdoptedClaudeMd(resolved, _PLUGIN_SLUG)
54
+ || _isAdoptedMemdir(_memdirPath(resolved), _PLUGIN_SLUG);
49
55
  } catch {
50
56
  return false;
51
57
  }
package/hook.mjs CHANGED
@@ -63,7 +63,7 @@ import { detectMemOverride } from './lib/mem-override.mjs';
63
63
  import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
64
64
  import { checkForUpdate, getCachedUpdateBanner, isUpdateCheckDue } from './hook-update.mjs';
65
65
  import { handleLLMOptimize } from './hook-optimize.mjs';
66
- import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
66
+ import { silentAutoAdopt } from './adopt-cli.mjs';
67
67
  import { emitV270UpgradeBanner } from './lib/upgrade-banner.mjs';
68
68
  import { loadCiteBackForEpisode, extractCiteBackSignals, buildUnsavedBugfixHint, countUnsavedBugfixShape, buildCiteRecallNudge as libBuildCiteRecallNudge, nextCiteLowStreak } from './lib/cite-back-hint.mjs';
69
69
  // plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
@@ -1074,36 +1074,32 @@ async function handleSessionStart() {
1074
1074
  }
1075
1075
  } catch (e) { debugCatch(e, 'session-start-cache-heal'); }
1076
1076
 
1077
- // First-run auto-adopt (v2.33.0 plugin-mode v2.82.1 install-mode-agnostic).
1077
+ // Auto-adopt + migrate (v3.13 CLAUDE.md-steering). silentAutoAdopt is now an
1078
+ // IDEMPOTENT per-session sync, so it runs on EVERY SessionStart — not gated by
1079
+ // the one-shot RUNTIME_DIR marker. That gate is deliberately gone: existing
1080
+ // users whose marker predates v3.13 must still get migrated (legacy memory-dir
1081
+ // sentinel stripped, CLAUDE.md managed block written) on their next session.
1082
+ // The sync short-circuits cheaply once a project is already on the new scheme.
1078
1083
  // ANY install path — `/plugin install`, `npm install -g`, `npx`, manual — is
1079
- // consent to integration. Writing the MEMORY.md sentinel once per project on
1080
- // first SessionStart avoids the opt-in friction that left ~zero users on
1081
- // auto-adopt (runtime-marker directory was empty machine-wide despite v2.33
1082
- // shipping ~5 weeks earlier — `install.mjs`-written hooks don't propagate
1083
- // ${CLAUDE_PLUGIN_ROOT}, so the v2.33.0 gate was a no-op for npm/manual
1084
- // installs, which is most of them). Scope is now:
1084
+ // consent to integration. Scope:
1085
1085
  // - gated by !MEM_NO_AUTO_ADOPT (explicit global escape hatch)
1086
- // - per-project opt-out via `<memdir>/.mem-no-auto-adopt` sentinel
1087
- // (managed by `claude-mem-lite adopt --disable / --enable`); checked
1088
- // inside silentAutoAdopt so the helper is safe to call directly too.
1089
- // - first-attempt marker persists in RUNTIME_DIR so a subsequent /unadopt
1090
- // is respected (no re-adopt loop).
1086
+ // - per-project opt-out via `<memdir>/.mem-no-auto-adopt` sentinel (managed
1087
+ // by `claude-mem-lite adopt --disable / --enable`; checked inside
1088
+ // silentAutoAdopt).
1089
+ // - CLAUDE_MEM_NO_TEMPLATE_REFRESH=1 freezes the block against drift refresh.
1091
1090
  // Note v2.82.0: removed MEM_QUIET_HOOKS gate. That env var suppresses stdout
1092
1091
  // noise; it must NOT also disable side-effect work (PostToolUse writes the
1093
- // DB unconditionally — auto-adopt should follow the same rule).
1094
- // Failures (user-edited sentinel, budget exceeded, FS errors) are swallowed;
1095
- // the marker is still written so we don't retry on every SessionStart.
1092
+ // DB unconditionally — auto-adopt follows the same rule). Failures are
1093
+ // swallowed; the marker is still written for telemetry/back-compat.
1096
1094
  try {
1097
1095
  if (process.env.MEM_NO_AUTO_ADOPT !== '1') {
1098
1096
  const project = inferProject();
1099
- if (!hasAutoAdoptMarker(RUNTIME_DIR, project)) {
1100
- const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
1101
- const r = silentAutoAdopt({ cwd, markerDir: RUNTIME_DIR, markerKey: project });
1102
- if (r.ok) {
1103
- debugLog('DEBUG', 'session-start-auto-adopt', `action=${r.action} project=${project}`);
1104
- } else {
1105
- debugLog('DEBUG', 'session-start-auto-adopt', `skipped project=${project} reason=${r.reason}`);
1106
- }
1097
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
1098
+ const r = silentAutoAdopt({ cwd, markerDir: RUNTIME_DIR, markerKey: project });
1099
+ if (r.ok) {
1100
+ debugLog('DEBUG', 'session-start-auto-adopt', `action=${r.action} project=${project}`);
1101
+ } else {
1102
+ debugLog('DEBUG', 'session-start-auto-adopt', `skipped project=${project} reason=${r.reason}`);
1107
1103
  }
1108
1104
  }
1109
1105
  } catch (e) { debugCatch(e, 'session-start-auto-adopt'); }
@@ -109,5 +109,5 @@ function buildAdoptHint({ projectPath, stubs }) {
109
109
  // stubs.adopted === true/false lets tests assert both branches without touching FS.
110
110
  const adopted = stubs && 'adopted' in stubs ? stubs.adopted : isAdoptedHere(projectPath);
111
111
  if (adopted) return null;
112
- return '🧷 Invited-memory 未启用:`claude-mem-lite adopt` 注入 MEMORY.md 上下文省 ~40%,MCP 调用率提升;静音 `MEM_NO_ADOPT_HINT=1`';
112
+ return '🧷 Invited-memory 未启用:`claude-mem-lite adopt` 写入 CLAUDE.md 托管块(通常每会话自动)→ 上下文省 ~40%,MCP 调用率提升;静音 `MEM_NO_ADOPT_HINT=1`';
113
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.12.1",
3
+ "version": "3.13.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
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -45,6 +45,7 @@
45
45
  "hook-precompact.mjs",
46
46
  "plugin-cache-guard.mjs",
47
47
  "memdir.mjs",
48
+ "claudemd.mjs",
48
49
  "adopt-content.mjs",
49
50
  "adopt-cli.mjs",
50
51
  "haiku-client.mjs",
package/server.mjs CHANGED
@@ -143,7 +143,7 @@ const _quiet = effectiveQuiet();
143
143
  if (process.env.CLAUDE_MEM_QUIET_TRACE !== '0') {
144
144
  const reason = process.env.MEM_QUIET_HOOKS === '1'
145
145
  ? 'env:MEM_QUIET_HOOKS=1'
146
- : _quiet ? 'adopted:MEMORY.md-sentinel' : 'none';
146
+ : _quiet ? 'adopted:steering' : 'none';
147
147
  const mode = _quiet ? 'BASE' : 'BASE+VERBOSE';
148
148
  process.stderr.write(`[mem] instructions: ${mode} reason=${reason}\n`);
149
149
  }
@@ -1715,7 +1715,7 @@ server.registerTool(
1715
1715
  // and direct MCP clients), but only the core tools appear in the `tools/list`
1716
1716
  // response. Hiding the maintenance/admin tools keeps Claude Code's startup
1717
1717
  // context small while preserving the contract that the plugin dogfoods (see
1718
- // CLAUDE.md §Mem usage contract and adopt-content.mjs).
1718
+ // the CLAUDE.md managed block + adopt-content.mjs detail doc).
1719
1719
  // Surface counts as of v2.70.0: 9 core (mem_search/recent/timeline/get/save/
1720
1720
  // recall + mem_defer/mem_defer_list/mem_defer_drop) + 11 hidden (maintenance/
1721
1721
  // admin/specialized) = 20 registered; tests/tool-schemas.test.mjs is the
package/source-files.mjs CHANGED
@@ -87,7 +87,9 @@ export const SOURCE_FILES = [
87
87
  'cli/activity.mjs',
88
88
  'server/fts-check.mjs',
89
89
  // v2.32 invited-memory: memdir primitives + adopt/unadopt CLI
90
+ // v3.13 CLAUDE.md-steering: claudemd.mjs project-tree managed block + migration
90
91
  'memdir.mjs',
92
+ 'claudemd.mjs',
91
93
  'adopt-content.mjs',
92
94
  'adopt-cli.mjs',
93
95
  // P0 (v2.59.x): user-explicit "ignore memory" override detector. Lives