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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +150 -185
- package/adopt-content.mjs +91 -48
- package/claudemd.mjs +244 -0
- package/commands/adopt.md +43 -36
- package/commands/unadopt.md +25 -11
- package/hook-shared.mjs +13 -7
- package/hook.mjs +20 -24
- package/lib/startup-dashboard.mjs +1 -1
- package/package.json +2 -1
- package/server.mjs +2 -2
- package/source-files.mjs +2 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
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.
|
|
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
|
-
//
|
|
2
|
-
// claude-mem-lite adopt
|
|
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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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,
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
|
|
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(
|
|
110
|
-
const
|
|
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] ${
|
|
115
|
-
log(`
|
|
116
|
-
log(` detail
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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.
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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] ${
|
|
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
|
|
304
|
-
log(` CLAUDE_PLUGIN_ROOT = ${pluginRoot} (
|
|
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 —
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
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 (
|
|
334
|
-
log('[unadopt] no memdirs found');
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
315
|
+
if (all) return migrateAll(['--all', ...(force ? ['--force'] : []), ...(dryRun ? ['--dry-run'] : [])]);
|
|
337
316
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// detail doc. Kept separate
|
|
4
|
-
//
|
|
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
|
|
7
|
-
//
|
|
8
|
-
// bump
|
|
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 = '
|
|
15
|
+
export const CURRENT_SENTINEL_VERSION = 'v2';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
20
|
-
|
|
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
|
|
25
|
-
* auto-loaded by Claude Code — the
|
|
26
|
-
*
|
|
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
|
-
|
|
32
|
-
> 设计背景见 docs/plans/2026-04-16-invited-memory-pattern.md。
|
|
57
|
+
## 被动 recall(hook 已自动跑,你只需采纳)
|
|
33
58
|
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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="<路径>"
|
|
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
|
-
|
|
|
46
|
-
|
|
|
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
|
-
##
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
|
71
|
-
|
|
|
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
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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` —
|
|
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` —
|
|
29
|
-
|
|
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`
|
|
34
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
package/commands/unadopt.md
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unadopt
|
|
3
|
-
description: "Use when: user wants to remove the
|
|
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
|
|
9
|
-
|
|
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 -->
|
|
14
|
-
block from
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
- `--
|
|
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`)
|
|
28
|
-
|
|
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
|
|
12
|
-
// adopt-content.mjs is pure strings. No circular deps —
|
|
13
|
-
|
|
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
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
// -
|
|
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
|
|
1094
|
-
//
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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`
|
|
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.
|
|
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:
|
|
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
|
|
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
|