@lh8ppl/claude-memory-kit 0.3.5 → 0.4.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/README.md +6 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/package.json +3 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/auto-persona.mjs +4 -1
- package/src/compress-session.mjs +13 -1
- package/src/config-core.mjs +7 -9
- package/src/decisions-journal.mjs +71 -3
- package/src/doctor.mjs +86 -4
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +34 -3
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +16 -3
- package/src/kiro-cli-agent.mjs +270 -0
- package/src/kiro-constants.mjs +19 -0
- package/src/kiro-hook-bin.mjs +105 -0
- package/src/kiro-hook-command.mjs +67 -0
- package/src/kiro-hook-dispatch.mjs +115 -0
- package/src/kiro-ide-hooks.mjs +219 -0
- package/src/kiro-permissions.mjs +175 -0
- package/src/kiro-skills.mjs +96 -0
- package/src/kiro-transcript.mjs +366 -0
- package/src/kiro-trusted-commands.mjs +130 -0
- package/src/managed-block.mjs +138 -0
- package/src/memory-write.mjs +23 -8
- package/src/mutate-agent-config.mjs +243 -0
- package/src/read-json.mjs +43 -0
- package/src/reindex.mjs +15 -2
- package/src/repair.mjs +39 -3
- package/src/result-shapes.mjs +8 -0
- package/src/review-queue.mjs +3 -0
- package/src/scratchpad.mjs +12 -2
- package/src/search.mjs +12 -5
- package/src/semantic-backend.mjs +7 -9
- package/src/settings-hooks.mjs +12 -2
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +48 -1
- package/src/weekly-curate.mjs +6 -2
- package/template/.claude/skills/memory-search/SKILL.md +14 -1
- package/template/.claude/skills/memory-write/SKILL.md +37 -1
- package/template/project/memory/INDEX.md.template +1 -1
package/src/repair.mjs
CHANGED
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
// Per design §14 + tasks.md 39 (39.1–39.3).
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
readFileSync,
|
|
19
21
|
statSync,
|
|
20
22
|
unlinkSync,
|
|
23
|
+
writeFileSync,
|
|
21
24
|
} from 'node:fs';
|
|
22
25
|
import { join } from 'node:path';
|
|
23
26
|
import {
|
|
@@ -204,6 +207,34 @@ async function repairIndex({ projectRoot, userDir, reindexer }) {
|
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Migrate committed memory markdown to the lint-clean shape (Task 164.9) — so a
|
|
212
|
+
* repo installed before the lint-clean generators (164.1–164.7) brings its
|
|
213
|
+
* existing memory up to the clean format a default markdownlint passes. Today
|
|
214
|
+
* this normalizes `context/DECISIONS.md` (old `### ` entries → `## ` +
|
|
215
|
+
* blank-surrounded). Idempotent + CRLF-tolerant: a no-op on already-clean
|
|
216
|
+
* content (changed:false), so it's safe to run repeatedly. ONLY rewrites a file
|
|
217
|
+
* whose normalized form actually differs — never a gratuitous write.
|
|
218
|
+
*/
|
|
219
|
+
async function repairMemoryFormat({ projectRoot }) {
|
|
220
|
+
const { normalizeDecisionsJournal } = await import('./decisions-journal.mjs');
|
|
221
|
+
const decisionsPath = join(projectRoot, 'context', 'DECISIONS.md');
|
|
222
|
+
if (!existsSync(decisionsPath)) {
|
|
223
|
+
return { kind: 'format', changed: false, detail: 'no DECISIONS.md' };
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const before = readFileSync(decisionsPath, 'utf8');
|
|
227
|
+
const after = normalizeDecisionsJournal(before);
|
|
228
|
+
if (after === before) {
|
|
229
|
+
return { kind: 'format', changed: false, detail: 'DECISIONS.md already lint-clean' };
|
|
230
|
+
}
|
|
231
|
+
writeFileSync(decisionsPath, after, 'utf8');
|
|
232
|
+
return { kind: 'format', changed: true, detail: 'DECISIONS.md migrated to lint-clean headings' };
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { kind: 'format', changed: false, error: `format migration failed: ${err?.message ?? err}` };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
207
238
|
/**
|
|
208
239
|
* Public boundary: run the repair pipeline.
|
|
209
240
|
*
|
|
@@ -226,15 +257,15 @@ export async function runRepair({
|
|
|
226
257
|
duration_ms: Date.now() - t0,
|
|
227
258
|
});
|
|
228
259
|
}
|
|
229
|
-
if (!['hooks', 'locks', 'index', 'all'].includes(scope)) {
|
|
260
|
+
if (!['hooks', 'locks', 'index', 'format', 'all'].includes(scope)) {
|
|
230
261
|
return errorResult({
|
|
231
262
|
category: ERROR_CATEGORIES.SCHEMA,
|
|
232
|
-
errors: [`invalid scope: ${scope}; expected 'hooks' | 'locks' | 'index' | 'all'`],
|
|
263
|
+
errors: [`invalid scope: ${scope}; expected 'hooks' | 'locks' | 'index' | 'format' | 'all'`],
|
|
233
264
|
duration_ms: Date.now() - t0,
|
|
234
265
|
});
|
|
235
266
|
}
|
|
236
267
|
|
|
237
|
-
const scopes = scope === 'all' ? ['hooks', 'locks', 'index'] : [scope];
|
|
268
|
+
const scopes = scope === 'all' ? ['hooks', 'locks', 'index', 'format'] : [scope];
|
|
238
269
|
const repairs = [];
|
|
239
270
|
let errors = 0;
|
|
240
271
|
for (const s of scopes) {
|
|
@@ -251,6 +282,11 @@ export async function runRepair({
|
|
|
251
282
|
const r = await repairIndex({ projectRoot, userDir, reindexer });
|
|
252
283
|
if (r.error) errors += 1;
|
|
253
284
|
repairs.push(r);
|
|
285
|
+
} else if (s === 'format') {
|
|
286
|
+
// eslint-disable-next-line no-await-in-loop
|
|
287
|
+
const r = await repairMemoryFormat({ projectRoot });
|
|
288
|
+
if (r.error) errors += 1;
|
|
289
|
+
repairs.push(r);
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
292
|
return {
|
package/src/result-shapes.mjs
CHANGED
|
@@ -110,6 +110,14 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
110
110
|
// NO silent fallback to keyword — the user asked for semantic,
|
|
111
111
|
// and the surface should fail-loud so they know what's missing.
|
|
112
112
|
SEMANTIC_UNAVAILABLE: 'semantic_unavailable',
|
|
113
|
+
|
|
114
|
+
// A per-agent config file (Task 50, cross-agent install) could not be
|
|
115
|
+
// PARSED — it exists but its bytes aren't valid for the declared format
|
|
116
|
+
// (corrupt/hand-edited JSON, etc.). `mutateAgentConfig` returns this
|
|
117
|
+
// and REFUSES to write, so a malformed third-party config is never
|
|
118
|
+
// clobbered (the claude-mem rigor-drift bug class, inverted into a
|
|
119
|
+
// guarantee — design: cross-agent adapter seam note 2026-06-20 / D-180).
|
|
120
|
+
CONFIG_PARSE: 'config_parse',
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
export const ACTION_TYPES = Object.freeze({
|
package/src/review-queue.mjs
CHANGED
|
@@ -166,6 +166,9 @@ function serializeReviewQueue({ preamble, entries }) {
|
|
|
166
166
|
const lines = [...preamble];
|
|
167
167
|
for (const e of entries) {
|
|
168
168
|
lines.push(`## ${e.ts} — auto-extract (medium-trust, pending review)`);
|
|
169
|
+
lines.push(''); // blank line below the heading (MD022). SAFE: parseReviewQueue
|
|
170
|
+
// scans i+1..i+5 for the bullet, so a blank heading→bullet gap is tolerated;
|
|
171
|
+
// the provenance stays at bulletLine+1 (the pair is never split).
|
|
169
172
|
lines.push(`- (${e.id}) ${e.text}`);
|
|
170
173
|
if (e.provenance) lines.push(e.provenance);
|
|
171
174
|
lines.push('');
|
package/src/scratchpad.mjs
CHANGED
|
@@ -215,7 +215,13 @@ export function ensureSectionExists(scratchpadPath, sectionTitle) {
|
|
|
215
215
|
// No leading blank lines for an empty/whitespace-only file (the scaffolded
|
|
216
216
|
// scratchpads are never empty, but keep the output clean if one ever is).
|
|
217
217
|
const prefix = body ? `${body}\n\n` : '';
|
|
218
|
-
|
|
218
|
+
// Blank line AFTER the heading too (MD022 blanks-around-headings) — the first
|
|
219
|
+
// bullet appended into this section then lands after the blank, so the
|
|
220
|
+
// committed scratchpad is lint-clean. SAFE for readers: findSectionRange uses
|
|
221
|
+
// a whole-line trim-compare + insertIntoSection skips trailing blanks, so a
|
|
222
|
+
// blank under the heading doesn't change where bullets insert. The blank is
|
|
223
|
+
// between the HEADING and the bullet — never inside the bullet↔comment pair.
|
|
224
|
+
writeFileSync(scratchpadPath, `${prefix}## ${sectionTitle}\n\n`, 'utf8');
|
|
219
225
|
return { created: true };
|
|
220
226
|
}
|
|
221
227
|
|
|
@@ -271,7 +277,11 @@ function archiveEvictedBullets({ tierRoot, tier, scratchpad, evicted, now }) {
|
|
|
271
277
|
const archivePath = join(archiveDir, 'evicted-bullets.md');
|
|
272
278
|
const ts = now ?? nowIso();
|
|
273
279
|
const header = existsSync(archivePath) ? '' : EVICTED_ARCHIVE_HEADER;
|
|
274
|
-
|
|
280
|
+
// Blank line after the heading so the archive is lint-clean markdown (MD022
|
|
281
|
+
// blanks-around-headings) — generated memory passes a strict linter by
|
|
282
|
+
// construction. The preceding header (or the prior block's trailing `\n\n`)
|
|
283
|
+
// supplies the blank line above.
|
|
284
|
+
const block = `## Evicted ${ts} — consolidate(${scratchpad})\n\n${evicted
|
|
275
285
|
.map((e) => e.block)
|
|
276
286
|
.join('\n')}\n\n`;
|
|
277
287
|
appendFileSync(archivePath, header + block, 'utf8');
|
package/src/search.mjs
CHANGED
|
@@ -481,12 +481,19 @@ function runDecisionsKeywordSearch(_db, opts) {
|
|
|
481
481
|
// The line offset of the marker = source_line drill-back into DECISIONS.md.
|
|
482
482
|
const sourceLine = content.slice(0, start).split('\n').length;
|
|
483
483
|
// Retracted-tag detection mirrors the WRITER's contract: the tag sits on its
|
|
484
|
-
// own line DIRECTLY after the
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
484
|
+
// own line DIRECTLY after the `## ` heading (decisions-journal.mjs §2 —
|
|
485
|
+
// buildDecisionEntry emits `## ` h2 entries; the retract inserter puts the
|
|
486
|
+
// tag at headingEnd+1), so scope the check there — NOT a raw-block substring,
|
|
487
|
+
// which would mislabel an active entry whose Why merely MENTIONS "_(retracted"
|
|
488
|
+
// (skill-review I1). Match the heading line-start (`\n## `) so body text
|
|
489
|
+
// containing `##` can't be mistaken for the heading. (Was `### ` — a
|
|
490
|
+
// pre-existing bug: the writer emits `## `, so this never matched and EVERY
|
|
491
|
+
// decision read `retracted:false` — Task 164.3.)
|
|
492
|
+
// The heading is a line-start `## ` (the block opens with the marker comment,
|
|
493
|
+
// so the heading is never at block offset 0 — match `\n## `).
|
|
494
|
+
const headingNl = block.indexOf('\n## ');
|
|
488
495
|
const afterHeading =
|
|
489
|
-
|
|
496
|
+
headingNl === -1 ? '' : block.slice(block.indexOf('\n', headingNl + 1) + 1);
|
|
490
497
|
const retracted = afterHeading.startsWith('_(retracted');
|
|
491
498
|
hits.push({
|
|
492
499
|
id: markers[i].id,
|
package/src/semantic-backend.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// ≤1500-char chunking rule is satisfied by construction).
|
|
29
29
|
|
|
30
30
|
import { createHash } from 'node:crypto';
|
|
31
|
-
import {
|
|
31
|
+
import { parseJsonFile } from './read-json.mjs';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
|
|
34
34
|
// The D-105 ladder's WINNER (bake-off 2026-06-10, bench:recall on the Task-99
|
|
@@ -383,14 +383,12 @@ const VALID_DEFAULT_MODES = new Set(['keyword', 'semantic', 'hybrid']);
|
|
|
383
383
|
* default — no surprise model downloads on machines that never opted in).
|
|
384
384
|
*/
|
|
385
385
|
export function resolveDefaultSearchMode({ projectRoot }) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return 'keyword';
|
|
393
|
-
}
|
|
386
|
+
// BOM-tolerant (parseJsonFile): a Windows-editor BOM on context/settings.json
|
|
387
|
+
// must not silently downgrade a `hybrid` user to keyword (D-187). Missing or
|
|
388
|
+
// malformed → keyword.
|
|
389
|
+
const p = join(projectRoot, 'context', 'settings.json');
|
|
390
|
+
const mode = parseJsonFile(p, { fallback: null })?.search?.default_mode;
|
|
391
|
+
return VALID_DEFAULT_MODES.has(mode) ? mode : 'keyword';
|
|
394
392
|
}
|
|
395
393
|
|
|
396
394
|
/**
|
package/src/settings-hooks.mjs
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
writeFileSync,
|
|
58
58
|
} from 'node:fs';
|
|
59
59
|
import { dirname, join } from 'node:path';
|
|
60
|
+
import { stripBom } from './read-json.mjs';
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Canonical npm-route hooks block. Shell form (no `args`), PATH-resolved
|
|
@@ -64,6 +65,10 @@ import { dirname, join } from 'node:path';
|
|
|
64
65
|
* (modulo command form) plugin/hooks/hooks.json.
|
|
65
66
|
*/
|
|
66
67
|
export const KIT_HOOKS_BLOCK = Object.freeze({
|
|
68
|
+
// PreToolUse — the memory delete-guardrail (D-192). Blocks a destructive
|
|
69
|
+
// shell command (rm / Remove-Item / git clean …) aimed at a memory path
|
|
70
|
+
// BEFORE it runs. The only kit hook that can exit non-zero (2 = block).
|
|
71
|
+
PreToolUse: [{ matcher: 'Bash|PowerShell', hooks: [{ type: 'command', command: 'cmk-guard-memory', timeout: 5 }] }],
|
|
67
72
|
SessionStart: [{ hooks: [{ type: 'command', command: 'cmk-inject-context', timeout: 30 }] }],
|
|
68
73
|
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'cmk-capture-prompt', timeout: 10 }] }],
|
|
69
74
|
PostToolUse: [{ matcher: 'Write|Edit|MultiEdit', hooks: [{ type: 'command', command: 'cmk-observe-edit', async: true, timeout: 120 }] }],
|
|
@@ -81,6 +86,7 @@ export const KIT_HOOKS_BLOCK = Object.freeze({
|
|
|
81
86
|
*/
|
|
82
87
|
export const KIT_COMMAND_TOKENS = Object.freeze([
|
|
83
88
|
'cmk-version-check',
|
|
89
|
+
'cmk-guard-memory',
|
|
84
90
|
'cmk-inject-context',
|
|
85
91
|
'cmk-capture-prompt',
|
|
86
92
|
'cmk-observe-edit',
|
|
@@ -122,7 +128,9 @@ export function writeKitHooks(settingsPath) {
|
|
|
122
128
|
let settings = {};
|
|
123
129
|
if (existsSync(settingsPath)) {
|
|
124
130
|
try {
|
|
125
|
-
|
|
131
|
+
// stripBom: a Windows-editor BOM must not make a valid settings.json read
|
|
132
|
+
// as corrupt and block hook wiring (D-187). Real corruption still errors.
|
|
133
|
+
settings = JSON.parse(stripBom(readFileSync(settingsPath, 'utf8')));
|
|
126
134
|
} catch (err) {
|
|
127
135
|
return {
|
|
128
136
|
changed: false,
|
|
@@ -250,7 +258,9 @@ export function writeKitMcpServer(projectRoot) {
|
|
|
250
258
|
let config = {};
|
|
251
259
|
if (existsSync(mcpPath)) {
|
|
252
260
|
try {
|
|
253
|
-
|
|
261
|
+
// stripBom: a BOM'd .mcp.json must not read as corrupt and block MCP
|
|
262
|
+
// registration (D-187). Real corruption still errors.
|
|
263
|
+
config = JSON.parse(stripBom(readFileSync(mcpPath, 'utf8')));
|
|
254
264
|
} catch (err) {
|
|
255
265
|
return { changed: false, path: mcpPath, error: `${mcpPath} parse error: ${err?.message ?? err}` };
|
|
256
266
|
}
|