@lh8ppl/claude-memory-kit 0.2.1 → 0.2.3
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 +7 -6
- package/bin/cmk-capture-prompt.mjs +17 -17
- package/bin/cmk-capture-turn.mjs +22 -21
- package/bin/cmk-compress-session.mjs +2 -2
- package/bin/cmk-inject-context.mjs +11 -11
- package/bin/cmk-observe-edit.mjs +17 -16
- package/package.json +1 -1
- package/src/audit-log.mjs +1 -0
- package/src/auto-extract.mjs +258 -6
- package/src/auto-persona.mjs +40 -8
- package/src/capture-turn.mjs +48 -1
- package/src/compress-session.mjs +89 -26
- package/src/compressor.mjs +1 -1
- package/src/conflict-queue.mjs +14 -0
- package/src/doctor.mjs +3 -3
- package/src/forget.mjs +29 -0
- package/src/graduation.mjs +1 -1
- package/src/index-rebuild.mjs +42 -0
- package/src/inject-context.mjs +5 -1
- package/src/install.mjs +29 -6
- package/src/lazy-compress.mjs +58 -9
- package/src/mcp-server.mjs +353 -124
- package/src/merge-facts.mjs +4 -0
- package/src/persona-portability.mjs +24 -1
- package/src/read-core.mjs +87 -0
- package/src/register-crons.mjs +64 -33
- package/src/remember-core.mjs +91 -0
- package/src/review-queue.mjs +13 -0
- package/src/rich-fact.mjs +46 -0
- package/src/settings-hooks.mjs +56 -2
- package/src/subcommands.mjs +419 -182
- package/src/weekly-curate.mjs +5 -0
- package/src/write-fact.mjs +25 -1
- package/template/.claude/skills/memory-write/SKILL.md +52 -35
- package/template/.gitignore.fragment +9 -3
- package/template/CLAUDE.md.template +2 -2
- package/template/docs/journey/journey-log.md.template +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
- **Cross-project persona — the wedge (v0.2)** — when you state how you work *everywhere* ("always use uv, never pip", "from now on run the linter before committing"), the per-turn auto-extract promotes it into your **user tier** (`~/.claude-memory-kit/`) **that turn**. So a brand-new project **cold-opens already knowing your style** — layered structure, your tooling, your testing discipline — with no hand-curation and no waiting. Carry it between your own machines with `cmk persona export`/`import`, or pin a single fact across projects with `cmk lessons promote`.
|
|
8
8
|
- **Frozen snapshot at session start** — MEMORY.md + USER.md + SOUL.md + INDEX.md + today's session log inject once at the first tool call, so Claude sees your context every session without you re-telling it.
|
|
9
|
-
- **Auto-extract on every assistant turn** — a background `claude --print` subagent reads each turn and saves durable facts (
|
|
10
|
-
- **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently.
|
|
11
|
-
- **Search + MCP** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads)
|
|
12
|
-
- **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows.
|
|
9
|
+
- **Auto-extract on every assistant turn** — a background `claude --print` subagent reads each turn and saves durable facts to memory. Durable project knowledge (setup/config, conventions, workflows, tool quirks) becomes a **rich Why/How fact file** (structured + searchable); lighter signals stay terse `MEMORY.md` bullets. Runs automatically, so the rich tier survives even when the model uses Claude Code's built-in memory instead. No manual writes needed.
|
|
10
|
+
- **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently. For backtick/quote-heavy rich facts, capture them shell-safe as JSON: `cmk remember --from-file fact.json` (or `--json` from stdin) — content never touches the shell.
|
|
11
|
+
- **Search + MCP — Claude runs every memory op for you, in conversation** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads). `cmk install` registers the kit's **MCP server**, so Claude can do the whole memory surface as tools without you ever typing `cmk`: capture (`mk_remember`, rich Why/How too), recall (`mk_search` / `mk_get` / `mk_timeline` / `mk_cite`), adjust trust (`mk_trust`), promote a fact across projects (`mk_lessons_promote`), forget (`mk_forget` — previews first, then deletes on confirm), and clear the review/conflict queues (`mk_queue_list` / `mk_queue_resolve`). The tools are allow-listed on install, so they run prompt-free.
|
|
12
|
+
- **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows. The session-buffer rollup self-heals at session start too, so memory stays bounded even if you never cleanly close the window.
|
|
13
13
|
- **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
|
|
14
14
|
- **9 health checks** — `cmk doctor` validates install, hook wiring, distill freshness, INDEX consistency, cron registration, and stale locks.
|
|
15
15
|
|
|
@@ -26,7 +26,7 @@ cmk install # scaffolds context/ + the memory-write skill AND wires the l
|
|
|
26
26
|
cmk doctor # verify, then restart Claude Code
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. No separate `/plugin` step needed. Use `cmk install --no-hooks`
|
|
29
|
+
`cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. It also **registers the kit's MCP server** in `.mcp.json` and allow-lists its tools (`mcp__cmk__*`) in `.claude/settings.json`, so Claude can drive memory as tools with no per-call prompt. No separate `/plugin` step needed. Use `cmk install --no-hooks` to skip the hooks + MCP wiring (scaffold-only).
|
|
30
30
|
|
|
31
31
|
> Installing the package globally adds the `cmk` CLI **and** the installer. It's the `cmk install` *subcommand* that wires the hooks — not the bare `npm install`.
|
|
32
32
|
|
|
@@ -51,9 +51,10 @@ Most-used commands (full list via `cmk --help`):
|
|
|
51
51
|
| `cmk doctor` | Run HC-1..HC-9 health checks, surface repair commands |
|
|
52
52
|
| `cmk repair --hooks` / `--locks` / `--index` / `--all` | Idempotent self-repair |
|
|
53
53
|
| `cmk search "<query>" [--mode keyword\|semantic\|hybrid]` | Search accumulated memory (keyword default) |
|
|
54
|
+
| `cmk get <id…>` / `cmk timeline <id>` / `cmk cite <id>` / `cmk recent-activity` | Read the index back — full fact bodies + provenance, sequential context around an observation, a canonical citation link, recent changes (the CLI side of the `mk_*` MCP read tools) |
|
|
54
55
|
| `cmk roll --scope now\|today\|recent` | Manually trigger a compression pipeline |
|
|
55
56
|
| `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly jobs with cron / launchd / Task Scheduler |
|
|
56
|
-
| `cmk forget <id>` | Tombstone a fact (
|
|
57
|
+
| `cmk forget <id>` | Tombstone a fact — disappears from `cmk search` immediately, no manual reindex (audit trail preserved) |
|
|
57
58
|
| `cmk lessons promote <id> [--to USER.md\|HABITS.md]` | Promote one captured fact to your cross-project **user tier** (the safe path — sanitized, secret-screened, audited) so it applies in **every** project |
|
|
58
59
|
| `cmk disable-native-memory` / `enable-native-memory` | Opt out of Claude Code's built-in Auto Memory so the kit is your single, lean memory layer (committable — travels with `git clone`) |
|
|
59
60
|
| `cmk persona generate` | Run cross-project persona synthesis on demand (instead of waiting for the weekly pass) |
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
// transcript, emit {"continue": true}. Always exit 0 — a hook that errors
|
|
12
12
|
// would interrupt the user mid-prompt.
|
|
13
13
|
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
15
14
|
import { dirname, join } from 'node:path';
|
|
16
15
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
16
|
|
|
@@ -19,35 +18,36 @@ function emitContinue() {
|
|
|
19
18
|
process.stdout.write('{"continue": true}');
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
emitContinue();
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
24
|
+
const modulePath = join(__dirname, '..', 'src', 'capture-prompt.mjs');
|
|
29
25
|
|
|
30
|
-
let
|
|
26
|
+
let readHookStdin;
|
|
27
|
+
let capturePrompt;
|
|
31
28
|
try {
|
|
32
|
-
|
|
29
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
30
|
+
({ capturePrompt } = await import(pathToFileURL(modulePath).href));
|
|
33
31
|
} catch (err) {
|
|
34
32
|
process.stderr.write(
|
|
35
|
-
`cmk-capture-prompt: failed to
|
|
33
|
+
`cmk-capture-prompt: failed to load modules: ${err?.message ?? err}\n`,
|
|
36
34
|
);
|
|
37
35
|
emitContinue();
|
|
38
36
|
process.exit(0);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// Drain the hook payload — but NOT on an interactive TTY (a manual run):
|
|
40
|
+
// a blocking stdin read would hang forever on a console that never sends EOF, before
|
|
41
|
+
// any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
|
|
42
|
+
// for a TTY so a manual invocation finishes instead of hanging.
|
|
43
|
+
const rawInput = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
44
44
|
|
|
45
|
-
let
|
|
45
|
+
let payload;
|
|
46
46
|
try {
|
|
47
|
-
(
|
|
47
|
+
payload = rawInput.trim() === '' ? {} : JSON.parse(rawInput);
|
|
48
48
|
} catch (err) {
|
|
49
49
|
process.stderr.write(
|
|
50
|
-
`cmk-capture-prompt: failed to
|
|
50
|
+
`cmk-capture-prompt: failed to parse stdin JSON: ${err?.message ?? err}\n`,
|
|
51
51
|
);
|
|
52
52
|
emitContinue();
|
|
53
53
|
process.exit(0);
|
package/bin/cmk-capture-turn.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// append to transcripts, spawn detached auto-extract, emit
|
|
14
14
|
// {"continue": true}, exit 0 within ~50ms (NFR-1). Always exit 0.
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
|
|
@@ -21,27 +21,9 @@ function emitContinue() {
|
|
|
21
21
|
process.stdout.write('{"continue": true}');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
let raw = '';
|
|
25
|
-
try {
|
|
26
|
-
raw = readFileSync(0, 'utf8');
|
|
27
|
-
} catch {
|
|
28
|
-
emitContinue();
|
|
29
|
-
process.exit(0);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let payload;
|
|
33
|
-
try {
|
|
34
|
-
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
process.stderr.write(
|
|
37
|
-
`cmk-capture-turn: failed to parse stdin JSON: ${err?.message ?? err}\n`,
|
|
38
|
-
);
|
|
39
|
-
emitContinue();
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
44
25
|
const __dirname = dirname(__filename);
|
|
26
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
45
27
|
const modulePath = join(__dirname, '..', 'src', 'capture-turn.mjs');
|
|
46
28
|
|
|
47
29
|
// Auto-extract path: env override → sibling cmk-auto-extract.mjs (ships
|
|
@@ -53,12 +35,31 @@ const autoExtractPath =
|
|
|
53
35
|
? join(__dirname, 'cmk-auto-extract.mjs')
|
|
54
36
|
: null);
|
|
55
37
|
|
|
38
|
+
let readHookStdin;
|
|
56
39
|
let captureTurn;
|
|
57
40
|
try {
|
|
41
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
58
42
|
({ captureTurn } = await import(pathToFileURL(modulePath).href));
|
|
59
43
|
} catch (err) {
|
|
60
44
|
process.stderr.write(
|
|
61
|
-
`cmk-capture-turn: failed to load
|
|
45
|
+
`cmk-capture-turn: failed to load modules: ${err?.message ?? err}\n`,
|
|
46
|
+
);
|
|
47
|
+
emitContinue();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Drain the hook payload — but NOT on an interactive TTY (a manual run):
|
|
52
|
+
// a blocking stdin read would hang forever on a console that never sends EOF, before
|
|
53
|
+
// any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
|
|
54
|
+
// for a TTY so a manual invocation finishes instead of hanging.
|
|
55
|
+
const raw = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
56
|
+
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
`cmk-capture-turn: failed to parse stdin JSON: ${err?.message ?? err}\n`,
|
|
62
63
|
);
|
|
63
64
|
emitContinue();
|
|
64
65
|
process.exit(0);
|
|
@@ -43,8 +43,8 @@ try {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Drain the hook payload so Claude Code's pipe closes cleanly — but NOT when
|
|
46
|
-
// stdin is an interactive TTY (a manual run):
|
|
47
|
-
// forever on a console that never sends EOF,
|
|
46
|
+
// stdin is an interactive TTY (a manual run): a blocking stdin read would hang
|
|
47
|
+
// forever on a console that never sends EOF, before any of the body
|
|
48
48
|
// runs (DECISION-LOG 2026-06-06). The payload is discarded; we read state from
|
|
49
49
|
// disk. readHookStdin returns '' for a TTY so a manual invocation finishes.
|
|
50
50
|
readHookStdin({ isTTY: process.stdin.isTTY });
|
|
@@ -14,19 +14,13 @@
|
|
|
14
14
|
// unconditionally — a throwing SessionStart hook would interrupt
|
|
15
15
|
// session start, worse than an empty additionalContext.
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
21
|
-
// Drain stdin so callers blocking on EPIPE don't hang.
|
|
22
|
-
try {
|
|
23
|
-
readFileSync(0, 'utf8');
|
|
24
|
-
} catch {
|
|
25
|
-
// stdin not connected; fine.
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
22
|
const __dirname = dirname(__filename);
|
|
23
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
30
24
|
const modulePath = join(__dirname, '..', 'src', 'inject-context.mjs');
|
|
31
25
|
|
|
32
26
|
// Resolve the sibling lazy-compress bin (ships in this same bin/ dir) so
|
|
@@ -39,14 +33,14 @@ const compressLazyPath =
|
|
|
39
33
|
? join(__dirname, 'cmk-compress-lazy.mjs')
|
|
40
34
|
: null);
|
|
41
35
|
|
|
36
|
+
let readHookStdin;
|
|
42
37
|
let injectContext;
|
|
43
38
|
try {
|
|
39
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
44
40
|
({ injectContext } = await import(pathToFileURL(modulePath).href));
|
|
45
41
|
} catch (err) {
|
|
46
42
|
process.stderr.write(
|
|
47
|
-
`cmk-inject-context: failed to load
|
|
48
|
-
err?.message ?? String(err)
|
|
49
|
-
}\n`,
|
|
43
|
+
`cmk-inject-context: failed to load modules: ${err?.message ?? String(err)}\n`,
|
|
50
44
|
);
|
|
51
45
|
process.stdout.write(
|
|
52
46
|
JSON.stringify({
|
|
@@ -59,6 +53,12 @@ try {
|
|
|
59
53
|
process.exit(0);
|
|
60
54
|
}
|
|
61
55
|
|
|
56
|
+
// Drain stdin so callers blocking on EPIPE don't hang — but NOT on an
|
|
57
|
+
// interactive TTY (a manual run): a blocking stdin read would hang forever on a
|
|
58
|
+
// console that never sends EOF (Task 101; DECISION-LOG 2026-06-06). The payload
|
|
59
|
+
// is discarded; readHookStdin returns '' for a TTY so a manual run finishes.
|
|
60
|
+
readHookStdin({ isTTY: process.stdin.isTTY });
|
|
61
|
+
|
|
62
62
|
try {
|
|
63
63
|
const r = injectContext({ cwd: process.cwd(), compressLazyPath });
|
|
64
64
|
process.stdout.write(JSON.stringify(r.hookOutput));
|
package/bin/cmk-observe-edit.mjs
CHANGED
|
@@ -11,37 +11,38 @@
|
|
|
11
11
|
// must never surface in the user's session. The append is
|
|
12
12
|
// fire-and-forget by design.
|
|
13
13
|
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
15
14
|
import { dirname, join } from 'node:path';
|
|
16
15
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
process.exit(0);
|
|
23
|
-
}
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
20
|
+
const modulePath = join(__dirname, '..', 'src', 'observe-edit.mjs');
|
|
24
21
|
|
|
25
|
-
let
|
|
22
|
+
let readHookStdin;
|
|
23
|
+
let observeEdit;
|
|
26
24
|
try {
|
|
27
|
-
|
|
25
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
26
|
+
({ observeEdit } = await import(pathToFileURL(modulePath).href));
|
|
28
27
|
} catch (err) {
|
|
29
28
|
process.stderr.write(
|
|
30
|
-
`cmk-observe-edit: failed to
|
|
29
|
+
`cmk-observe-edit: failed to load modules: ${err?.message ?? err}\n`,
|
|
31
30
|
);
|
|
32
31
|
process.exit(0);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// Drain the hook payload — but NOT on an interactive TTY (a manual run):
|
|
35
|
+
// a blocking stdin read would hang forever on a console that never sends EOF, before
|
|
36
|
+
// any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
|
|
37
|
+
// for a TTY so a manual invocation finishes instead of hanging.
|
|
38
|
+
const raw = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
38
39
|
|
|
39
|
-
let
|
|
40
|
+
let payload;
|
|
40
41
|
try {
|
|
41
|
-
(
|
|
42
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
42
43
|
} catch (err) {
|
|
43
44
|
process.stderr.write(
|
|
44
|
-
`cmk-observe-edit: failed to
|
|
45
|
+
`cmk-observe-edit: failed to parse stdin JSON: ${err?.message ?? err}\n`,
|
|
45
46
|
);
|
|
46
47
|
process.exit(0);
|
|
47
48
|
}
|
package/package.json
CHANGED
package/src/audit-log.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export const AUDIT_LOG_SCHEMA_VERSION = 1;
|
|
|
30
30
|
// they come online; the rule is one canonical machine-parseable token per
|
|
31
31
|
// kind of audit event (not a free-text reason field).
|
|
32
32
|
export const REASON_CODES = Object.freeze({
|
|
33
|
+
FACT_CREATED: 'fact-created', // writeFact: a new fact file was written (Task 123.A — the default create audit; callers emitting a richer code opt out via audit:false)
|
|
33
34
|
DUPLICATE: 'duplicate', // writeFact: same path + same id
|
|
34
35
|
DUPLICATE_ELSEWHERE: 'duplicate-elsewhere', // writeFact: different path + same id
|
|
35
36
|
USER_REQUESTED: 'user-requested', // forget: user-initiated tombstone
|