claude-mem-lite 6.11.0 → 6.12.1
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/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/cli/common.mjs +33 -2
- package/cli/verify-apply.mjs +196 -0
- package/cli-path.mjs +11 -1
- package/cli.mjs +1 -0
- package/commands/adopt.md +1 -1
- package/commands/bug.md +1 -1
- package/commands/lesson.md +1 -1
- package/commands/mem.md +8 -8
- package/commands/unadopt.md +1 -1
- package/commands/verify.md +156 -0
- package/hook-optimize.mjs +91 -16
- package/install.mjs +13 -10
- package/lib/atomic-write.mjs +5 -3
- package/lib/get-core.mjs +11 -3
- package/lib/install-shape.mjs +1 -1
- package/lib/native-binding-hint.mjs +1 -1
- package/lib/observation-write.mjs +1 -1
- package/lib/verify-apply-core.mjs +641 -0
- package/mem-cli.mjs +18 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +7 -4
- package/scripts/hook-launcher.mjs +1 -1
- package/scripts/pre-tool-recall.js +4 -2
- package/source-files.mjs +5 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.12.1",
|
|
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. 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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.12.1",
|
|
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/README.md
CHANGED
|
@@ -165,7 +165,7 @@ is still outside it gets a message naming both sides of the mismatch instead of
|
|
|
165
165
|
|
|
166
166
|
Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
|
|
167
167
|
|
|
168
|
-
> **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (
|
|
168
|
+
> **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <cmd>`), so they work without anything on your `PATH`. A global `claude-mem-lite` **shell** command (for running queries yourself in a terminal) is **optional** — `npm i -g claude-mem-lite` — and is a *separate* npm install: the plugin's auto-update does **not** refresh it, so re-run `npm i -g claude-mem-lite@latest` if you want that shell command kept in sync. You do **not** need it for the plugin to be fully functional.
|
|
169
169
|
|
|
170
170
|
> **Auto-adopt writes into your project, on every SessionStart (v3.13+).** The plugin adds a slug-scoped **managed block** to your project's own **`<cwd>/CLAUDE.md`** — a file that is normally committed to git — plus a `<cwd>/.claude/plugin_claude_mem_lite.md` detail file. The block is a system-authority pointer that boosts Claude's proactive use of `mem_recall` / `mem_save`. Everything outside the block is preserved verbatim, and it coexists with other plugins' blocks in the same file ([details](#invited-memory-v232)). This happens on **every** SessionStart, not just the first: the sync is idempotent and re-applies the block if it is edited away, and refreshes it when the shipped template changes. It applies regardless of install path (npm, npx, `/plugin`, manual), so **no manual `/adopt` is needed**.
|
|
171
171
|
>
|
|
@@ -405,6 +405,7 @@ surface — reach them through the CLI column in the second table.
|
|
|
405
405
|
/mem <query> # Shorthand for search
|
|
406
406
|
/lesson <text> # Save a non-obvious lesson to the events table (v2.31.0)
|
|
407
407
|
/bug <text> # Log a known bug + repro steps to the events table (v2.31.0)
|
|
408
|
+
/verify # Check memories against the current code; correct stale ones after you approve
|
|
408
409
|
```
|
|
409
410
|
|
|
410
411
|
### Efficient Search Workflow
|
package/README.zh-CN.md
CHANGED
|
@@ -129,7 +129,7 @@ v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`
|
|
|
129
129
|
|
|
130
130
|
插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
|
|
131
131
|
|
|
132
|
-
> **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI
|
|
132
|
+
> **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI(`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <cmd>`),因此不依赖 `PATH` 上的任何东西。全局 `claude-mem-lite` **shell** 命令(用于你自己在终端里跑查询)是**可选**的 —— `npm i -g claude-mem-lite` —— 且是**独立**的 npm 安装:插件的自动更新**不会**刷新它,想保持同步就重新跑 `npm i -g claude-mem-lite@latest`。插件要完整工作**并不需要**它。
|
|
133
133
|
|
|
134
134
|
### 方式二:npx(一行命令)
|
|
135
135
|
|
|
@@ -341,6 +341,7 @@ README 和 `docs/ARCHITECTURE.md` 都钉在它上面。)
|
|
|
341
341
|
/mem <query> # search 的简写
|
|
342
342
|
/lesson <text> # 保存非显而易见的经验到 events 表(v2.31.0)
|
|
343
343
|
/bug <text> # 记录已知 bug + 复现步骤到 events 表(v2.31.0)
|
|
344
|
+
/verify # 对照当前代码核查记忆;你确认后才改正过期的记忆
|
|
344
345
|
```
|
|
345
346
|
|
|
346
347
|
### 高效搜索工作流
|
package/cli/common.mjs
CHANGED
|
@@ -399,6 +399,13 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
399
399
|
// warn-on-every-unknown-flag flip turned the omission into a false warning on a
|
|
400
400
|
// documented, working command.
|
|
401
401
|
'prompts-limit',
|
|
402
|
+
// `verify-apply --apply --digest <d>` / `--undo <backup>` — read as flags.apply / .digest / .undo in
|
|
403
|
+
// cli/verify-apply.mjs. Missing here, a working apply printed "Unknown flag --apply — ignored,
|
|
404
|
+
// it had no effect" beside its own read-back. Pinned by tests/verify-apply-cli.test.mjs.
|
|
405
|
+
'apply',
|
|
406
|
+
'digest',
|
|
407
|
+
'print-project',
|
|
408
|
+
'undo',
|
|
402
409
|
// Entries here MUST be read by a `claude-mem-lite` subcommand. A flag that no
|
|
403
410
|
// command reads is worse than an absent one: it converts the "ignored, it had no
|
|
404
411
|
// effect" warning into silence, so the user's dropped flag reads as accepted.
|
|
@@ -410,6 +417,21 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
410
417
|
// --has?" suggestion. Locked by tests/cli-flag-allowlist.test.mjs.
|
|
411
418
|
]);
|
|
412
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Flags exactly ONE subcommand reads, mapped to that command. KNOWN_CLI_FLAGS is a union, so
|
|
422
|
+
* without this `recent --apply` was accepted in silence while `--apply` did nothing there.
|
|
423
|
+
* Every other command mem-cli dispatches now reports such a flag, and the typo suggester stops
|
|
424
|
+
* offering it there. Install-family commands (doctor, status, …) route to install.mjs and are
|
|
425
|
+
* not covered. Only flags with a single reader
|
|
426
|
+
* belong here; tests/cli-flag-allowlist.test.mjs derives that from the sources.
|
|
427
|
+
*/
|
|
428
|
+
export const COMMAND_SCOPED_FLAGS = new Map([
|
|
429
|
+
['apply', 'verify-apply'],
|
|
430
|
+
['digest', 'verify-apply'],
|
|
431
|
+
['print-project', 'verify-apply'],
|
|
432
|
+
['undo', 'verify-apply'],
|
|
433
|
+
]);
|
|
434
|
+
|
|
413
435
|
/** Levenshtein distance, early-exit past `max` (cheap enough for a handful of flags). */
|
|
414
436
|
function editDistance(a, b, max = 2) {
|
|
415
437
|
const m = a.length,
|
|
@@ -437,16 +459,25 @@ function editDistance(a, b, max = 2) {
|
|
|
437
459
|
* project — a typo produced a wrong result with zero signal. Returns [{flag, suggestion}].
|
|
438
460
|
* Unknown flags with NO close match are omitted: they may be a valid flag we didn't
|
|
439
461
|
* catalog, so silence beats a false alarm. Warning-only by contract — never fails.
|
|
462
|
+
* A flag in COMMAND_SCOPED_FLAGS given to any other `cmd` is reported too, with `owner` set.
|
|
440
463
|
* @param {object} flags Parsed flags from parseArgs.
|
|
441
|
-
* @
|
|
464
|
+
* @param {string} [cmd] The subcommand being run; without it scoped flags are not checked.
|
|
465
|
+
* @returns {Array<{flag: string, suggestion: string|null, owner?: string}>}
|
|
442
466
|
*/
|
|
443
|
-
export function suggestUnknownFlags(flags) {
|
|
467
|
+
export function suggestUnknownFlags(flags, cmd) {
|
|
444
468
|
const result = [];
|
|
445
469
|
for (const key of Object.keys(flags)) {
|
|
470
|
+
const owner = COMMAND_SCOPED_FLAGS.get(key);
|
|
471
|
+
if (cmd && owner && owner !== cmd) {
|
|
472
|
+
result.push({ flag: key, suggestion: null, owner });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
446
475
|
if (!key || KNOWN_CLI_FLAGS.has(key)) continue;
|
|
447
476
|
let best = null,
|
|
448
477
|
bestDist = 3;
|
|
449
478
|
for (const known of KNOWN_CLI_FLAGS) {
|
|
479
|
+
const knownOwner = COMMAND_SCOPED_FLAGS.get(known);
|
|
480
|
+
if (cmd && knownOwner && knownOwner !== cmd) continue;
|
|
450
481
|
const d = editDistance(key, known);
|
|
451
482
|
if (d < bestDist) {
|
|
452
483
|
bestDist = d;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// cli/verify-apply.mjs — `claude-mem-lite verify-apply <proposals.json> [--project P]`
|
|
2
|
+
// (dry run), `... --apply --digest <d>`, and `claude-mem-lite verify-apply --undo <backup.json>`.
|
|
3
|
+
//
|
|
4
|
+
// The write step of /verify (commands/verify.md). The agent proposes; this command is the only
|
|
5
|
+
// thing that writes. It defaults to a dry run that prints the new text itself and a digest, and
|
|
6
|
+
// --apply refuses unless it is handed that digest back: what the user approved is what lands,
|
|
7
|
+
// and a proposals file or a row that changed in between is refused rather than applied. All
|
|
8
|
+
// policy lives in lib/verify-apply-core.mjs; this file is I/O, formatting and exit codes.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { DB_DIR } from '../lib/data-paths.mjs';
|
|
13
|
+
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
14
|
+
import { inferProject } from '../utils.mjs';
|
|
15
|
+
import { resolveProject } from '../project-utils.mjs';
|
|
16
|
+
import {
|
|
17
|
+
parseProposals,
|
|
18
|
+
planVerifyApply,
|
|
19
|
+
planDigest,
|
|
20
|
+
priorVerifyApplies,
|
|
21
|
+
runVerifyApply,
|
|
22
|
+
undoVerifyBackup,
|
|
23
|
+
markUndone,
|
|
24
|
+
} from '../lib/verify-apply-core.mjs';
|
|
25
|
+
import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
|
|
26
|
+
import { shellWord } from '../cli-path.mjs';
|
|
27
|
+
|
|
28
|
+
const USAGE =
|
|
29
|
+
'[mem] Usage: claude-mem-lite verify-apply <proposals.json> [--project P] (dry run)\n' +
|
|
30
|
+
' claude-mem-lite verify-apply <proposals.json> [--project P] --apply --digest <d>\n' +
|
|
31
|
+
' claude-mem-lite verify-apply --undo <backup.json>';
|
|
32
|
+
|
|
33
|
+
const SNIPPET_CONTEXT = 60;
|
|
34
|
+
const BACKUP_DIR = join(DB_DIR, 'backups');
|
|
35
|
+
// The commands this prints must run as printed. `claude-mem-lite` is on PATH only after an
|
|
36
|
+
// optional global npm install (which may also be a different, stale code home), so name the
|
|
37
|
+
// node binary and THIS cli.mjs — the one that produced the plan.
|
|
38
|
+
const SELF = process.argv[1] ? `node ${shellWord(process.argv[1])}` : 'claude-mem-lite';
|
|
39
|
+
|
|
40
|
+
function readJson(path, what) {
|
|
41
|
+
try {
|
|
42
|
+
return { value: JSON.parse(readFileSync(path, 'utf8')) };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { error: `[mem] Cannot read ${what} ${path}: ${e.message}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The changed region of a field, with context. The changed part is printed IN FULL, never
|
|
50
|
+
* clipped: the digest covers the whole text, so any cap here would let the user approve text
|
|
51
|
+
* they were never shown (re-review of dcc8f72, P2-2). Only unchanged context is elided.
|
|
52
|
+
*/
|
|
53
|
+
function snippet(before, after) {
|
|
54
|
+
const a = before === null || before === undefined ? '' : String(before);
|
|
55
|
+
const b = String(after);
|
|
56
|
+
if (a === b) return [' (unchanged)'];
|
|
57
|
+
let start = 0;
|
|
58
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
59
|
+
let endA = a.length;
|
|
60
|
+
let endB = b.length;
|
|
61
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
62
|
+
endA--;
|
|
63
|
+
endB--;
|
|
64
|
+
}
|
|
65
|
+
const from = Math.max(0, start - SNIPPET_CONTEXT);
|
|
66
|
+
const lead = from > 0 ? '…' : '';
|
|
67
|
+
const tail = (s, end) => (end + SNIPPET_CONTEXT < s.length ? '…' : '');
|
|
68
|
+
return [
|
|
69
|
+
` - ${lead}${a.slice(from, Math.min(a.length, endA + SNIPPET_CONTEXT))}${tail(a, endA)}`,
|
|
70
|
+
` + ${lead}${b.slice(from, Math.min(b.length, endB + SNIPPET_CONTEXT))}${tail(b, endB)}`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function describe(p) {
|
|
75
|
+
const head = ` #${p.id} [${p.verdict}] ${p.action} — evidence: ${p.evidence}`;
|
|
76
|
+
if (p.action === 'retire') return [head, ' retired with no replacement (kept as history)'];
|
|
77
|
+
const fields =
|
|
78
|
+
p.action === 'edit'
|
|
79
|
+
? Object.entries(p.set)
|
|
80
|
+
: ['title', 'narrative', 'lesson_learned', 'importance', 'facts', 'concepts']
|
|
81
|
+
.filter((k) => p[k] !== undefined)
|
|
82
|
+
.map((k) => [k, p[k]]);
|
|
83
|
+
const lines = [head];
|
|
84
|
+
if (p.action === 'replace') lines.push(' new memory supersedes this one; unlisted fields are copied');
|
|
85
|
+
for (const [k, v] of fields) lines.push(` ${k}:`, ...snippet(p.before[k], v));
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function undo(db, path) {
|
|
90
|
+
const { value, error } = readJson(path, 'backup');
|
|
91
|
+
if (error) return fail(error);
|
|
92
|
+
const { errors, restored } = undoVerifyBackup(db, value);
|
|
93
|
+
if (restored.length === 0 && errors.length)
|
|
94
|
+
return fail(`[mem] Undo refused, nothing written:\n ${errors.join('\n ')}`);
|
|
95
|
+
for (const r of restored) {
|
|
96
|
+
out(
|
|
97
|
+
` #${r.id} restored${r.replacementRetired ? ` (replacement #${r.replacementRetired} retired)` : ''}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (errors.length) return fail(`[mem] Undo read-back found problems:\n ${errors.join('\n ')}`);
|
|
101
|
+
out(`[mem] Undo complete: ${restored.length} row(s) restored.`);
|
|
102
|
+
// The restore is committed; marking the file only stops a second run early. If the mark
|
|
103
|
+
// cannot be written, a second undo is still refused (the rows no longer match the apply's
|
|
104
|
+
// record), so this is a warning, not a failure of the undo that already happened.
|
|
105
|
+
try {
|
|
106
|
+
atomicWriteFileSync(path, JSON.stringify(markUndone(value), null, 1));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
process.stderr.write(
|
|
109
|
+
`[mem] Warning: undo is done, but ${path} could not be marked as undone: ${e.message}\n`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function cmdVerifyApply(db, args) {
|
|
115
|
+
const { positional, flags } = parseArgs(args);
|
|
116
|
+
if (rejectBareStringFlags(flags, ['project', 'undo', 'digest'])) return;
|
|
117
|
+
// The project verify-apply targets when --project is omitted, printed so /verify can export
|
|
118
|
+
// exactly that project (export's --project matching is fuzzy; this is not).
|
|
119
|
+
if (flags['print-project'] === true) return out(inferProject());
|
|
120
|
+
// Boolean means boolean: `--apply=false` / `--apply no` must not apply.
|
|
121
|
+
if (flags.apply !== undefined && flags.apply !== true)
|
|
122
|
+
return fail(`[mem] --apply takes no value.\n${USAGE}`);
|
|
123
|
+
|
|
124
|
+
if (flags.undo !== undefined) {
|
|
125
|
+
if (positional.length || flags.apply || flags.digest) return fail(USAGE);
|
|
126
|
+
return undo(db, flags.undo);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const file = positional[0];
|
|
130
|
+
if (!file || positional.length > 1) return fail(USAGE);
|
|
131
|
+
const { value, error } = readJson(file, 'proposals');
|
|
132
|
+
if (error) return fail(error);
|
|
133
|
+
|
|
134
|
+
const parsed = parseProposals(value);
|
|
135
|
+
if (parsed.errors.length)
|
|
136
|
+
return fail(`[mem] Invalid proposals, nothing written:\n ${parsed.errors.join('\n ')}`);
|
|
137
|
+
|
|
138
|
+
const project = flags.project ? resolveProject(db, flags.project, { mode: 'write' }) : inferProject();
|
|
139
|
+
const { plan, errors } = planVerifyApply(db, parsed.entries, { project });
|
|
140
|
+
if (errors.length) return fail(`[mem] Refused, nothing written:\n ${errors.join('\n ')}`);
|
|
141
|
+
const digest = planDigest(
|
|
142
|
+
plan,
|
|
143
|
+
project,
|
|
144
|
+
priorVerifyApplies(
|
|
145
|
+
BACKUP_DIR,
|
|
146
|
+
plan.map((p) => p.id),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!flags.apply) {
|
|
151
|
+
out(`[mem] verify-apply plan — project ${project}, ${plan.length} change(s):`);
|
|
152
|
+
for (const p of plan) for (const line of describe(p)) out(line);
|
|
153
|
+
out(`[mem] Plan digest: ${digest}`);
|
|
154
|
+
out('[mem] Dry run — nothing written. After the user approves exactly this plan, run:');
|
|
155
|
+
out(
|
|
156
|
+
` ${SELF} verify-apply ${shellWord(file)} --project ${shellWord(project)} --apply --digest ${digest}`,
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!flags.digest) {
|
|
162
|
+
return fail('[mem] --apply requires --digest <d> from the dry run the user approved. Nothing written.');
|
|
163
|
+
}
|
|
164
|
+
if (flags.digest !== digest) {
|
|
165
|
+
return fail(
|
|
166
|
+
'[mem] Plan digest mismatch: the proposals file or the memories changed since the dry run. ' +
|
|
167
|
+
'Nothing written — re-run the dry run and get the new plan approved.',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let run;
|
|
172
|
+
try {
|
|
173
|
+
run = runVerifyApply(db, plan, { backupDir: BACKUP_DIR });
|
|
174
|
+
} catch (e) {
|
|
175
|
+
// One failure happens AFTER the commit: the undo record could not be written. The changes
|
|
176
|
+
// are in the database then, and saying "nothing written" would be false.
|
|
177
|
+
if (e.message.startsWith('applied, but')) {
|
|
178
|
+
return fail(
|
|
179
|
+
`[mem] APPLIED — the changes are in the database, but ${e.message.slice('applied, but '.length)}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return fail(`[mem] ${e.message}`);
|
|
183
|
+
}
|
|
184
|
+
for (const c of run.checks) {
|
|
185
|
+
out(
|
|
186
|
+
` #${c.id} ${c.action}${c.newId ? ` → #${c.newId}` : ''}: ${c.ok ? 'ok' : `MISMATCH (${c.problems.join('; ')})`}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
out(`[mem] Backup: ${run.backupPath}`);
|
|
190
|
+
out(
|
|
191
|
+
`[mem] To undo (only while these rows are untouched): ${SELF} verify-apply --undo ${shellWord(run.backupPath)}`,
|
|
192
|
+
);
|
|
193
|
+
if (run.checks.some((c) => !c.ok)) {
|
|
194
|
+
fail('[mem] Applied, but read-back found mismatches — show the MISMATCH lines above to the user.');
|
|
195
|
+
}
|
|
196
|
+
}
|
package/cli-path.mjs
CHANGED
|
@@ -22,4 +22,14 @@ import { join, dirname } from 'node:path';
|
|
|
22
22
|
// module from its unused-export report entirely. Enforced by
|
|
23
23
|
// tests/no-url-module-paths.test.mjs.
|
|
24
24
|
export const CLI_PATH = join(dirname(fileURLToPath(import.meta.url)), 'cli.mjs');
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A path as one shell word: as-is when it is plain, single-quoted otherwise. Conditional on
|
|
27
|
+
* purpose — CLI_INVOKE is LLM-visible (MCP instructions, every "Equivalent CLI" hint), so an
|
|
28
|
+
* ordinary install path must stay byte-identical; only a path with a space or a shell
|
|
29
|
+
* metacharacter gets quotes, which it needs to survive as ONE argument.
|
|
30
|
+
* @param {string} s
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
34
|
+
|
|
35
|
+
export const CLI_INVOKE = `node ${shellWord(CLI_PATH)}`;
|
package/cli.mjs
CHANGED
package/commands/adopt.md
CHANGED
|
@@ -64,4 +64,4 @@ server builds its instructions once at boot, so the MCP-instructions trim
|
|
|
64
64
|
(or re-attaches the mem-lite MCP server). A `/exit` + fresh session is enough.
|
|
65
65
|
Same caveat applies in reverse for `/unadopt`.
|
|
66
66
|
|
|
67
|
-
!node ${CLAUDE_PLUGIN_ROOT}/cli.mjs adopt $ARGUMENTS
|
|
67
|
+
!node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt $ARGUMENTS
|
package/commands/bug.md
CHANGED
|
@@ -49,7 +49,7 @@ goes in `--lesson` so it lands in the high-weight `lesson_learned` field.
|
|
|
49
49
|
body in the positional content and trim `--lesson` to the core description if
|
|
50
50
|
longer:
|
|
51
51
|
|
|
52
|
-
node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<body>" \
|
|
52
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" save "<body>" \
|
|
53
53
|
--type bugfix \
|
|
54
54
|
--title "<first 60 chars of description>" \
|
|
55
55
|
--lesson "<description, ≤500 chars>" \
|
package/commands/lesson.md
CHANGED
|
@@ -42,7 +42,7 @@ field. `--lesson` is capped at 500 chars (longer values are rejected), so for a
|
|
|
42
42
|
long lesson keep the full text in the positional content and trim `--lesson` to
|
|
43
43
|
the core insight:
|
|
44
44
|
|
|
45
|
-
node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<full text>" \
|
|
45
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" save "<full text>" \
|
|
46
46
|
--type discovery \
|
|
47
47
|
--title "<first 60 chars of text>" \
|
|
48
48
|
--lesson "<core insight, ≤500 chars>" \
|
package/commands/mem.md
CHANGED
|
@@ -23,16 +23,16 @@ Search and browse your project memory efficiently.
|
|
|
23
23
|
|
|
24
24
|
When the user invokes `/mem`, parse their intent:
|
|
25
25
|
|
|
26
|
-
- `/mem search <query>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
|
|
27
|
-
- `/mem recent` or `/mem recent 20` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recent [N]` via Bash
|
|
28
|
-
- `/mem recall <file>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recall <file>` via Bash
|
|
29
|
-
- `/mem timeline <id>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs timeline --anchor <id>` via Bash
|
|
26
|
+
- `/mem search <query>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" search <query>` via Bash
|
|
27
|
+
- `/mem recent` or `/mem recent 20` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" recent [N]` via Bash
|
|
28
|
+
- `/mem recall <file>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" recall <file>` via Bash
|
|
29
|
+
- `/mem timeline <id>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" timeline --anchor <id>` via Bash
|
|
30
30
|
- `/mem save <text>` → call `mem_save` MCP tool with the text as content
|
|
31
|
-
- `/mem stats` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs stats` via Bash
|
|
32
|
-
- `/mem get <ids>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <ids>` via Bash
|
|
31
|
+
- `/mem stats` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" stats` via Bash
|
|
32
|
+
- `/mem get <ids>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <ids>` via Bash
|
|
33
33
|
- `/mem cleanup` → run `mem_maintain(action="scan")`, report pending purge count and stale items to user, ask for confirmation, then run `mem_maintain(action="execute", operations=["purge_stale"], confirm=true)` if confirmed. **`confirm=true` is required and is not optional politeness:** without it the call returns a dry-run PREVIEW and deletes nothing, while still succeeding — so you would report a cleanup that never happened.
|
|
34
34
|
- `/mem cleanup Nd` (e.g. `60d`) → same as above but add `retain_days=N` to only purge items older than N days. **`retain_days` must be between 7 and 365**; anything outside that range is rejected by the schema, so `/mem cleanup 3d` cannot be honoured — say so rather than silently substituting the default.
|
|
35
35
|
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `retain_days=N`, same 7–365 range.
|
|
36
|
-
- `/mem <query>` (no subcommand) → treat as search, run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
|
|
36
|
+
- `/mem <query>` (no subcommand) → treat as search, run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" search <query>` via Bash
|
|
37
37
|
|
|
38
|
-
Use Bash commands first. For detailed data, use `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <id>` via Bash.
|
|
38
|
+
Use Bash commands first. For detailed data, use `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <id>` via Bash.
|
package/commands/unadopt.md
CHANGED
|
@@ -43,4 +43,4 @@ Once unadopted, the conservative hook layer (SessionStart `File Lessons` /
|
|
|
43
43
|
`Key Context`, MCP instructions `WHEN TO USE`) returns to verbose mode on the
|
|
44
44
|
next session start.
|
|
45
45
|
|
|
46
|
-
!node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS
|
|
46
|
+
!node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" unadopt $ARGUMENTS
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify
|
|
3
|
+
description: "Use when: the user explicitly asks to verify, audit or correct their stored memories against the current code (e.g. \"check my memories for stale ones\", /verify). You check each memory with read-only repo tools and propose corrections; nothing is written until the user approves the exact plan. Not for routine recall or saving."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /verify — check memories against the code, then correct the stale ones
|
|
7
|
+
|
|
8
|
+
Memories go stale: a bug recorded as open gets fixed, a measurement gets retracted, a
|
|
9
|
+
mechanism is replaced. Measured on 118 live memories across 7 repos, ~10% were STALE and
|
|
10
|
+
~14% PARTIAL, and most went stale within a day of being saved. No automatic pass catches
|
|
11
|
+
this — cheap single-shot models misjudged it (precision 0.36) and model-written corrections
|
|
12
|
+
were false in 28 of 72 cases. What works is YOU reading the code: you propose, the user
|
|
13
|
+
approves the exact plan, and `verify-apply` is the only thing that writes.
|
|
14
|
+
|
|
15
|
+
(If another plugin also defines `/verify`, this one is `/claude-mem-lite:verify`.)
|
|
16
|
+
|
|
17
|
+
## Arguments
|
|
18
|
+
|
|
19
|
+
- `--from <YYYY-MM-DD>`: only memories saved on or after this date (default: all live).
|
|
20
|
+
- `--ids 12,34`: only these memories.
|
|
21
|
+
- `--project <name>`: a project other than the current one.
|
|
22
|
+
|
|
23
|
+
## Step 1 — Select
|
|
24
|
+
|
|
25
|
+
Get the exact project name first. For the current project (the usual case), run from the
|
|
26
|
+
project's directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply --print-project
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
It prints the canonical name (e.g. `dev--my-app`) that verify-apply itself will target. For
|
|
33
|
+
another project, use the name the user gave only if it already has the `parent--name` shape;
|
|
34
|
+
otherwise ask. Always pass that exact name — `export --project` matches loosely and can pick a
|
|
35
|
+
neighbouring project from a short name.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" export --project <project> [--from <date>] > <scratch>/memories.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every row carries `id`, `project`, `type`, `title`, `narrative`, `facts`, `lesson_learned`,
|
|
42
|
+
`files_modified`, `created_at`. Check that every row's `project` is the name you passed. With
|
|
43
|
+
`--ids`, filter the file to those ids. The repository to check them against is the current
|
|
44
|
+
directory for the current project; for another project, ask the user where it lives.
|
|
45
|
+
`files_modified` holds both absolute and repo-relative paths. Tell the user how many memories
|
|
46
|
+
you are about to check.
|
|
47
|
+
|
|
48
|
+
## Step 2 — Verify (read-only)
|
|
49
|
+
|
|
50
|
+
For EACH memory, answer: *if this memory were shown to an agent working in this repo today,
|
|
51
|
+
would anything it asserts mislead that agent?*
|
|
52
|
+
|
|
53
|
+
1. List its concrete, checkable claims: identifiers, file paths, `file:line` references,
|
|
54
|
+
constants, counts, behaviours, "X is still broken", "Y was measured at Z".
|
|
55
|
+
2. Check each against the current tree (Grep, Read) and, where useful, the history since
|
|
56
|
+
`created_at` (`git log --since=<created_at> -- <files>`, `git log -S<token>`, `git show`).
|
|
57
|
+
3. Pick one verdict:
|
|
58
|
+
- **VALID** — every checkable claim still holds.
|
|
59
|
+
- **STALE** — at least one specific claim is contradicted by the tree or history: a
|
|
60
|
+
renamed/removed symbol, a changed value or default, a changed behaviour, a bug presented
|
|
61
|
+
as open that has since been fixed, a count that moved, a measurement later retracted.
|
|
62
|
+
- **PARTIAL** — the main point holds; a secondary detail (a line number, a count, a minor
|
|
63
|
+
mechanism) is out of date.
|
|
64
|
+
- **UNVERIFIABLE** / **NO_CODE_CLAIM** — leave these alone.
|
|
65
|
+
|
|
66
|
+
Rules that the measurement showed matter:
|
|
67
|
+
- A file having changed is NOT a contradiction. You need a line, a diff hunk, a commit, or a
|
|
68
|
+
command's output that contradicts a specific claim.
|
|
69
|
+
- Output you did not see is not a contradiction. A grep that printed nothing, or output cut
|
|
70
|
+
off by `head` or a size limit, proves nothing — re-run it untruncated before calling a claim
|
|
71
|
+
stale.
|
|
72
|
+
- A number only a test run could confirm (a test count, a coverage figure) is a dated
|
|
73
|
+
measurement: stale only if a later commit or document reports a different value, not
|
|
74
|
+
because you cannot re-run it here.
|
|
75
|
+
- A record of what happened (what was measured then, what a review found) stays true as
|
|
76
|
+
history. It is stale only if it would mislead about the PRESENT.
|
|
77
|
+
- A note that already records its own fix is not stale because the fix commit landed later.
|
|
78
|
+
- Do not write, stash, check out or run tests while verifying.
|
|
79
|
+
|
|
80
|
+
More than ~25 memories: split them into batches and give each batch to a read-only subagent
|
|
81
|
+
with the rubric above; have each subagent write its results to a file with a bash heredoc and
|
|
82
|
+
reply with only the path. Treat a subagent's verdict as a lead: before proposing anything,
|
|
83
|
+
re-open every cited `file:line`, commit or command yourself.
|
|
84
|
+
|
|
85
|
+
## Step 3 — Draft proposals (STALE and PARTIAL only)
|
|
86
|
+
|
|
87
|
+
If every memory is VALID (or UNVERIFIABLE / NO_CODE_CLAIM), stop here: report the counts to
|
|
88
|
+
the user and say nothing needs changing. Do not run Step 4 with an empty proposals file.
|
|
89
|
+
|
|
90
|
+
Write `<scratch>/proposals.json` — a JSON array, one entry per memory to change:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[
|
|
94
|
+
{ "id": 52, "action": "replace", "verdict": "STALE",
|
|
95
|
+
"title": "...", "narrative": "...", "lesson_learned": "...",
|
|
96
|
+
"evidence": "7bc8ba9; hook-optimize.mjs:1371-1383" },
|
|
97
|
+
{ "id": 64, "action": "edit", "verdict": "PARTIAL",
|
|
98
|
+
"set": { "narrative": "..." }, "evidence": "hook-llm.mjs:1359-1361" },
|
|
99
|
+
{ "id": 201, "action": "retire", "verdict": "STALE", "evidence": "600c744" }
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **edit** — the stale part is one detail in `title`, `narrative`, `lesson_learned`,
|
|
104
|
+
`importance` or `concepts`. Copy the original text into `set` and change only the stale words.
|
|
105
|
+
- **replace** — the memory's claim is wrong but its lesson is still worth keeping, OR the
|
|
106
|
+
stale detail sits in `facts` (edit cannot change `facts`). Write the corrected memory; give
|
|
107
|
+
`facts` (an empty string drops them) or `concepts` when those are what is stale. Omitted
|
|
108
|
+
fields are copied from the original, which stays as history.
|
|
109
|
+
- **retire** — nothing in it is worth keeping (e.g. a mid-debug note about a failure fixed
|
|
110
|
+
minutes later).
|
|
111
|
+
- `evidence` is required: the commit, the `file:line`, or the command and its output (for
|
|
112
|
+
something outside the repo, e.g. a tool version) that shows the memory is out of date.
|
|
113
|
+
- `lesson_learned` is at most 500 characters. Keep the memory's language.
|
|
114
|
+
|
|
115
|
+
## Step 4 — Show the plan and get approval
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply <scratch>/proposals.json --project <project>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This is a dry run: it validates every entry against the database, writes nothing, prints each
|
|
122
|
+
change as `-` old / `+` new text (the changed text in full), and ends with a **plan digest**
|
|
123
|
+
and the exact apply command.
|
|
124
|
+
Show the user that output as it is (do not summarise the changes away), plus your counts of
|
|
125
|
+
VALID / STALE / PARTIAL, and ask for approval. If the dry run refuses an entry, fix the
|
|
126
|
+
proposal — never work around it with `update`, `save` or `delete`. If you change the proposals
|
|
127
|
+
after the user saw them, run the dry run again and show the new plan: the digest changes, and
|
|
128
|
+
the old one will be refused.
|
|
129
|
+
|
|
130
|
+
## Step 5 — Apply, only after the user approves that plan
|
|
131
|
+
|
|
132
|
+
Run exactly the command the dry run printed:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" verify-apply <scratch>/proposals.json --project <project> --apply --digest <digest>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It refuses if the proposals or the memories changed since the dry run. Otherwise it backs up
|
|
139
|
+
every target row, applies all entries in one transaction (all or nothing), reads each row back
|
|
140
|
+
and prints `ok` or `MISMATCH`, then prints the backup path and the undo command. Report the
|
|
141
|
+
result and the undo command to the user. Reading the exit status:
|
|
142
|
+
- exit 0 — applied, every row read back `ok`.
|
|
143
|
+
- exit 1 with `MISMATCH` lines — applied, but a row did not read back as planned. Show those
|
|
144
|
+
lines; do not re-run the apply.
|
|
145
|
+
- exit 1 with a message starting `APPLIED` — the changes are in the database, but no undo
|
|
146
|
+
record could be written. Say exactly that.
|
|
147
|
+
- any other exit 1 — nothing was written. Say why.
|
|
148
|
+
|
|
149
|
+
Undo (`verify-apply --undo <backup>`) only works while the changed rows' content and state are
|
|
150
|
+
as the apply left them (usage counters such as access counts do not count): it refuses once any
|
|
151
|
+
of them changes again — an edit, a supersede, or a routine background pass (importance decay,
|
|
152
|
+
alias or concept backfill) — and it runs at most once. It then prints `Undo complete`; a `Warning: … could not be marked as undone` line means
|
|
153
|
+
the undo still happened. After an undo, the earlier apply command is refused: applying the
|
|
154
|
+
same changes again needs a new dry run and the user's approval of its new digest. That refusal
|
|
155
|
+
rests on the backup file: once it is deleted, the earlier command would match again, so never
|
|
156
|
+
re-run an apply command from an earlier approval.
|