@lh8ppl/claude-memory-kit 0.3.2 → 0.3.4
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 +8 -6
- package/package.json +1 -1
- package/src/auto-extract.mjs +16 -5
- package/src/claude-md.mjs +7 -2
- package/src/compress-retry.mjs +155 -0
- package/src/compress-session.mjs +29 -7
- package/src/compressor.mjs +19 -1
- package/src/daily-distill.mjs +22 -7
- package/src/doctor.mjs +23 -2
- package/src/import-claude-md.mjs +7 -2
- package/src/index-rebuild.mjs +91 -3
- package/src/inject-context.mjs +16 -6
- package/src/lazy-compress.mjs +86 -0
- package/src/mcp-server.mjs +9 -1
- package/src/read-core.mjs +65 -3
- package/src/remember-core.mjs +15 -15
- package/src/sanitize.mjs +30 -0
- package/src/search.mjs +119 -4
- package/src/session-end-tasks.mjs +40 -3
- package/src/subcommands.mjs +39 -5
- package/src/version-drift.mjs +72 -0
- package/src/weekly-curate.mjs +20 -7
- package/template/.claude/skills/memory-search/SKILL.md +20 -0
- package/template/CLAUDE.md.template +1 -0
package/src/subcommands.mjs
CHANGED
|
@@ -416,7 +416,19 @@ async function runSearch(queryParts, options) {
|
|
|
416
416
|
let mode = explicitMode ?? resolveDefaultSearchMode({ projectRoot });
|
|
417
417
|
// Task 104.2 — the L3 raw tier: `--scope transcripts` searches the
|
|
418
418
|
// separate transcript-chunk index (synthetic T: ids; no tier/trust).
|
|
419
|
+
// Task 156 — `--scope decisions` scans context/DECISIONS.md (the decision
|
|
420
|
+
// journal) for decision-history / "what did we reject" recall.
|
|
419
421
|
const scope = options?.scope ?? 'facts';
|
|
422
|
+
// Task 156 / v0.3.3 cut-gate-16: the `decisions` scope is keyword-only BY
|
|
423
|
+
// DESIGN — it scans the flat `context/DECISIONS.md` journal, which is NOT
|
|
424
|
+
// embedded (no vec table). So it can never go through the semantic backend.
|
|
425
|
+
// Coerce to keyword BEFORE the semantic block, silently — a user who has the
|
|
426
|
+
// hybrid default (from `--with-semantic`) must not see a scary
|
|
427
|
+
// "unknown-scope:decisions" warning (configured default) or hard exit-2
|
|
428
|
+
// (explicit --mode) for using a real, shipped scope. The recall just works.
|
|
429
|
+
if (scope === 'decisions') {
|
|
430
|
+
mode = SEARCH_MODES.KEYWORD;
|
|
431
|
+
}
|
|
420
432
|
let semanticBackend;
|
|
421
433
|
if (mode === SEARCH_MODES.SEMANTIC || mode === SEARCH_MODES.HYBRID) {
|
|
422
434
|
const { prepareSemanticBackend } = await import('./semantic-backend.mjs');
|
|
@@ -443,6 +455,7 @@ async function runSearch(queryParts, options) {
|
|
|
443
455
|
query,
|
|
444
456
|
mode,
|
|
445
457
|
scope,
|
|
458
|
+
projectRoot, // Task 156: the decisions scope reads context/DECISIONS.md
|
|
446
459
|
minTrust: options?.minTrust,
|
|
447
460
|
tier: options?.tier,
|
|
448
461
|
since: options?.since,
|
|
@@ -464,9 +477,15 @@ async function runSearch(queryParts, options) {
|
|
|
464
477
|
for (const hit of r.results) {
|
|
465
478
|
// Plain-text output suitable for terminal piping. Snippet uses
|
|
466
479
|
// FTS5's <b>...</b> markers; preserved as-is so callers can pipe
|
|
467
|
-
// to a TUI that renders them OR strip via sed.
|
|
468
|
-
//
|
|
469
|
-
|
|
480
|
+
// to a TUI that renders them OR strip via sed. Hits with no tier/trust
|
|
481
|
+
// (raw transcript chunks; decision-journal entries) show the scope's
|
|
482
|
+
// label instead — 'transcript' for the L3 raw tier, 'decision' for the
|
|
483
|
+
// journal (Task 156), plus a `(retracted)` marker so the "what did we
|
|
484
|
+
// reject" trail is visible at a glance.
|
|
485
|
+
let provenance;
|
|
486
|
+
if (hit.tier) provenance = `${hit.tier}/${hit.trust}`;
|
|
487
|
+
else if (r.scope === 'decisions') provenance = hit.retracted ? 'decision (retracted)' : 'decision';
|
|
488
|
+
else provenance = 'transcript';
|
|
470
489
|
console.log(
|
|
471
490
|
`${hit.id}\t${provenance}\t${hit.source_file}:${hit.source_line}\t${hit.snippet}`,
|
|
472
491
|
);
|
|
@@ -512,10 +531,19 @@ export function withReadDb(fn, deps = {}) {
|
|
|
512
531
|
}
|
|
513
532
|
}
|
|
514
533
|
|
|
515
|
-
export function runGet(ids,
|
|
534
|
+
export function runGet(ids, options = {}, _command, deps = {}) {
|
|
516
535
|
const log = deps.log ?? console.log;
|
|
517
536
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
518
|
-
|
|
537
|
+
// Task 155 (D-163): `--include-tombstoned` is the HUMAN-only recovery opt-in.
|
|
538
|
+
// It's a CLI flag ONLY — the MCP mk_get tool never exposes it, so automatic
|
|
539
|
+
// recall stays tombstone-blind. projectRoot is resolved the same way
|
|
540
|
+
// withReadDb does, so the tombstone-file fallback can find the archive.
|
|
541
|
+
const includeTombstoned = options.includeTombstoned === true;
|
|
542
|
+
const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
|
|
543
|
+
const rows = withReadDb(
|
|
544
|
+
(db) => getObservations(db, list, { includeTombstoned, projectRoot }),
|
|
545
|
+
deps,
|
|
546
|
+
);
|
|
519
547
|
log(JSON.stringify(rows, null, 2));
|
|
520
548
|
// All-missing/invalid → exit 2 (lets a script tell "nothing matched" from a hit).
|
|
521
549
|
if (rows.length > 0 && rows.every((r) => r.error)) process.exitCode = 2;
|
|
@@ -1999,6 +2027,12 @@ export const subcommands = [
|
|
|
1999
2027
|
description: 'fetch full observation bodies + provenance by ID (parity with the mk_get MCP tool)',
|
|
2000
2028
|
milestone: 108,
|
|
2001
2029
|
argSpec: [{ flags: '<ids...>', description: 'one or more citation IDs (e.g. P-S79MJHFN)' }],
|
|
2030
|
+
optionSpec: [
|
|
2031
|
+
{
|
|
2032
|
+
flags: '--include-tombstoned',
|
|
2033
|
+
description: 'also recover forgotten (tombstoned) facts from the archive — human-only; the AI never reads tombstones',
|
|
2034
|
+
},
|
|
2035
|
+
],
|
|
2002
2036
|
action: runGet,
|
|
2003
2037
|
},
|
|
2004
2038
|
{
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// HC-9: version-drift detection (Task 162 / D-176).
|
|
2
|
+
//
|
|
3
|
+
// WHY: after a user updates the global `cmk` (npm i -g @latest), a project's
|
|
4
|
+
// version-stamped scaffold — the CLAUDE.md managed block, the hooks, the skills —
|
|
5
|
+
// stays at the OLD version until `cmk install` re-runs in that project. Updating the
|
|
6
|
+
// npm package ALONE does not touch a project (the per-project re-install is the
|
|
7
|
+
// easily-forgotten step). Pre-162 the kit was silent about it (D-172: no update path).
|
|
8
|
+
// HC-9 makes `cmk doctor` TELL the user the project is behind + the exact command.
|
|
9
|
+
//
|
|
10
|
+
// The project's installed version lives in the CLAUDE.md managed-block start marker
|
|
11
|
+
// (`<!-- claude-memory-kit:start v0.3.3 -->`); the installed binary version is
|
|
12
|
+
// getKitVersion(). Drift = binary NEWER than the project marker → "run cmk install".
|
|
13
|
+
// A project marker NEWER than the binary is a downgrade (older global cli opening a
|
|
14
|
+
// newer-scaffolded project), NOT drift — flag pass, not a false alarm.
|
|
15
|
+
|
|
16
|
+
import { findManagedBlock, compareVersions } from './claude-md.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pure HC-9 check. Injectable inputs (no disk read here) so the logic is unit-tested
|
|
20
|
+
* without a fixture tree; the doctor wiring reads CLAUDE.md + getKitVersion() and
|
|
21
|
+
* passes them in.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args
|
|
24
|
+
* @param {string|null} args.claudeMdText — the project's CLAUDE.md content, or null if absent.
|
|
25
|
+
* @param {string} args.kitVersion — the installed binary version (getKitVersion()).
|
|
26
|
+
* @returns {{id:'HC-9', name:string, status:'pass'|'fail'|'skip', message:string, recoveryCommand?:string}}
|
|
27
|
+
*/
|
|
28
|
+
export function checkVersionDrift({ claudeMdText, kitVersion } = {}) {
|
|
29
|
+
const id = 'HC-9';
|
|
30
|
+
const name = 'Project scaffold version matches the installed cmk';
|
|
31
|
+
|
|
32
|
+
// No CLAUDE.md, or no managed block → the project isn't kit-installed (or the block
|
|
33
|
+
// was hand-removed). Not a drift signal; skip (HC-1/repair owns the missing-block case).
|
|
34
|
+
if (!claudeMdText) {
|
|
35
|
+
return { id, name, status: 'skip', message: 'no CLAUDE.md found — project not kit-installed' };
|
|
36
|
+
}
|
|
37
|
+
const block = findManagedBlock(claudeMdText);
|
|
38
|
+
if (!block) {
|
|
39
|
+
return { id, name, status: 'skip', message: 'no claude-memory-kit managed block in CLAUDE.md' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `block.version` is the `:start vX` marker value (findManagedBlock recovers it
|
|
43
|
+
// even from a corrupted/orphan-start block — a stale corrupted block still earns
|
|
44
|
+
// the `cmk install` advice, which fixes both). compareVersions strips any
|
|
45
|
+
// `-prerelease` tag, so a `v0.3.4-beta` scaffold reads as `0.3.4` (the kit ships
|
|
46
|
+
// no prereleases today; this is the intended "close enough" behavior).
|
|
47
|
+
const projectVersion = block.version;
|
|
48
|
+
const cmp = compareVersions(kitVersion, projectVersion);
|
|
49
|
+
|
|
50
|
+
if (cmp <= 0) {
|
|
51
|
+
// Binary == project (match) OR binary < project (a downgrade — older cli, newer
|
|
52
|
+
// scaffold). Neither is "re-run install to catch up." Pass.
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
name,
|
|
56
|
+
status: 'pass',
|
|
57
|
+
message:
|
|
58
|
+
cmp === 0
|
|
59
|
+
? `project scaffold (v${projectVersion}) matches the installed cmk (v${kitVersion})`
|
|
60
|
+
: `project scaffold (v${projectVersion}) is newer than the installed cmk (v${kitVersion}) — likely an older global cli; not drift`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Binary NEWER than the project marker → the project is stale. THE drift case.
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
name,
|
|
68
|
+
status: 'fail',
|
|
69
|
+
message: `this project's scaffold is v${projectVersion} but your installed cmk is v${kitVersion} — re-run \`cmk install\` here to refresh the CLAUDE.md block, hooks, and skills (then restart Claude Code)`,
|
|
70
|
+
recoveryCommand: 'cmk install',
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/weekly-curate.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import { canonicalize } from '@lh8ppl/cmk-canonicalize';
|
|
|
39
39
|
import { nowIso } from './audit-log.mjs';
|
|
40
40
|
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
41
41
|
import { HaikuTimeoutError } from './compressor.mjs';
|
|
42
|
+
import { compressWithRetry } from './compress-retry.mjs';
|
|
42
43
|
import {
|
|
43
44
|
DEFAULT_COOLDOWN_MS,
|
|
44
45
|
isCooldownActive,
|
|
@@ -388,14 +389,21 @@ export async function weeklyCurate({
|
|
|
388
389
|
const sourceDates = old.map((f) => f.date);
|
|
389
390
|
|
|
390
391
|
let result;
|
|
392
|
+
let retries = 0; // Task 161.12: count retries so the log shows the retry RATE.
|
|
391
393
|
try {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
// Task 161 / D-175: ceiling-free path (cron/detached child, NO 60s hook ceiling)
|
|
395
|
+
// → bounded transient-only retry (maxAttempts:2 = one retry). See compress-retry.mjs.
|
|
396
|
+
result = await compressWithRetry(
|
|
397
|
+
backend,
|
|
398
|
+
{
|
|
399
|
+
input: buffer,
|
|
400
|
+
instructions,
|
|
401
|
+
preserveCitationIds: true,
|
|
402
|
+
maxOutputBytes: archiveMaxBytes,
|
|
403
|
+
timeoutMs: 50_000,
|
|
404
|
+
},
|
|
405
|
+
{ maxAttempts: 2, onRetry: () => { retries += 1; } },
|
|
406
|
+
);
|
|
399
407
|
touchCooldownMarker({ projectRoot, now: ts });
|
|
400
408
|
} catch (err) {
|
|
401
409
|
touchCooldownMarker({ projectRoot, now: ts });
|
|
@@ -418,6 +426,10 @@ export async function weeklyCurate({
|
|
|
418
426
|
duration_ms,
|
|
419
427
|
success: false,
|
|
420
428
|
error_category: errorCategory,
|
|
429
|
+
// Task 161 (D-173 observability): structured failure reason — see compress-session.mjs.
|
|
430
|
+
...(err?.exitCode != null ? { exit_code: err.exitCode } : {}),
|
|
431
|
+
...(err?.stderr ? { error_detail: String(err.stderr).slice(0, 500) } : {}),
|
|
432
|
+
...(retries > 0 ? { retries } : {}), // 161.12: failed AFTER retrying
|
|
421
433
|
},
|
|
422
434
|
});
|
|
423
435
|
return errorResult({
|
|
@@ -487,6 +499,7 @@ export async function weeklyCurate({
|
|
|
487
499
|
archived_days: old.length,
|
|
488
500
|
current_days: current.length,
|
|
489
501
|
recent_rebuild_action: recentResult?.action ?? 'skipped',
|
|
502
|
+
...(retries > 0 ? { retries } : {}), // 161.12: succeeded after a transient retry
|
|
490
503
|
...(deletionErrors.length > 0 ? { deletion_errors: deletionErrors } : {}),
|
|
491
504
|
},
|
|
492
505
|
});
|
|
@@ -63,6 +63,26 @@ Hits are raw turn excerpts (dialogue + the tools the agent ran), keyed
|
|
|
63
63
|
whole turns. If something found here is durably useful, say so in the
|
|
64
64
|
summary so the caller can capture it as a proper fact.
|
|
65
65
|
|
|
66
|
+
## Decision HISTORY — the `decisions` scope
|
|
67
|
+
|
|
68
|
+
For "what did we DECIDE about X" a normal fact search (steps 1-3) is enough —
|
|
69
|
+
the decision fact carries its own **Why**. But when the question is about how a
|
|
70
|
+
decision **evolved**, what we **reject**ed or moved away from, or **why X
|
|
71
|
+
changed** ("did we ever consider Y?", "weren't we using Postgres?", "what did
|
|
72
|
+
we decide and did it change?"), search the **decision journal** — the
|
|
73
|
+
append-only `context/DECISIONS.md`, which keeps superseded + retracted entries
|
|
74
|
+
the live fact store no longer carries:
|
|
75
|
+
|
|
76
|
+
- MCP: `mk_search` with `scope: "decisions"`.
|
|
77
|
+
- CLI: `cmk search "<topic>" --scope decisions`
|
|
78
|
+
|
|
79
|
+
Hits are decision entries keyed by their fact id, labelled `decision` (or
|
|
80
|
+
`decision (retracted)` for a reversed one). The retracted/superseded entries
|
|
81
|
+
ARE the answer to "what did we reject" — surface them explicitly, with the
|
|
82
|
+
date, so the caller sees the trail. Use this scope IN ADDITION to the fact
|
|
83
|
+
ladder when the question has a history/evolution axis; the fact search answers
|
|
84
|
+
the "current decision", the journal answers "how it got there".
|
|
85
|
+
|
|
66
86
|
## When the query is vague
|
|
67
87
|
|
|
68
88
|
If you cannot form a concrete query, look at recent activity first, then
|
|
@@ -29,6 +29,7 @@ The `cmk doctor` health checks verify each layer is wired correctly: install int
|
|
|
29
29
|
The snapshot injected at session start is a **bounded hot index, not everything** — there is a deeper, queryable archive. When a question is "what did we decide / what's our X / how does the user work / what's the setup / **how is this project structured or built / where does X live / what's the architecture**," **query your memory instead of re-deriving the answer from scratch** — the structure is a recorded decision, recall it before re-reading the files to reconstruct it:
|
|
30
30
|
|
|
31
31
|
- **`cmk search "<topic>"`** — find any captured fact (decisions, preferences, config, lessons) across the project + user tiers.
|
|
32
|
+
- **`cmk search "<topic>" --scope decisions`** — the append-only **decision journal** (`context/DECISIONS.md`). Use it for decision **history / evolution** — "what did we reject", "did X change", "why did we move away from Y" — it keeps superseded + retracted decisions the live fact store drops. (A plain `cmk search` answers "what's the current decision"; this answers "how it got there".)
|
|
32
33
|
- **`context/memory/<type>_<slug>.md`** — the granular fact archive with full **Why / How** rationale (`context/memory/INDEX.md` lists them).
|
|
33
34
|
- **`~/.claude-memory-kit/` (`USER.md` / `HABITS.md` / `LESSONS.md`)** — how this user works across *all* their projects.
|
|
34
35
|
|