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