@lh8ppl/claude-memory-kit 0.3.5 → 0.4.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/README.md +137 -50
- package/bin/cmk-approve-permission.mjs +62 -0
- package/bin/cmk-daily-distill.mjs +14 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/bin/cmk-inject-context.mjs +12 -0
- package/bin/cmk-weekly-curate.mjs +12 -0
- package/package.json +4 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/approve-permission.mjs +92 -0
- package/src/auto-extract.mjs +17 -10
- package/src/auto-persona.mjs +11 -4
- package/src/compaction-state.mjs +204 -0
- 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 +128 -5
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +42 -18
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +53 -7
- 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/lazy-compress.mjs +43 -110
- 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/register-crons.mjs +31 -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 +70 -3
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +82 -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/README.md
CHANGED
|
@@ -5,91 +5,178 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>Persistent, per-project memory for <a href="https://docs.claude.com/en/docs/claude-code">Claude Code</a> — plain markdown, committed with your code, recalled by meaning.</strong>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@lh8ppl/claude-memory-kit"><img src="https://img.shields.io/npm/v/@lh8ppl/claude-memory-kit?label=npm&color=blue" alt="npm"></a>
|
|
14
|
+
<a href="https://github.com/LH8PPL/claude-memory-kit/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="License: MIT"></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/Node.js-bundled%20%C2%B7%20none%20required-brightgreen" alt="Node.js bundled, none required">
|
|
16
|
+
<a href="https://github.com/LH8PPL/claude-memory-kit/actions/workflows/ci.yml"><img src="https://github.com/LH8PPL/claude-memory-kit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://img.shields.io/badge/Windows-supported-0078D6?logo=windows&logoColor=white" alt="Windows supported">
|
|
21
|
+
<img src="https://img.shields.io/badge/macOS-supported-000000?logo=apple&logoColor=white" alt="macOS supported">
|
|
22
|
+
<img src="https://img.shields.io/badge/Linux-supported-FCC624?logo=linux&logoColor=black" alt="Linux supported">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://img.shields.io/badge/Claude_Code-supported-6E56CF" alt="Claude Code supported">
|
|
27
|
+
<img src="https://img.shields.io/badge/Kiro-supported-6E56CF" alt="Kiro supported">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
Claude forgets everything when a session ends — so every new chat you re-explain who you are, what you're building, and how you like things done. **claude-memory-kit** fixes that: it quietly captures your decisions, preferences, and project context, then hands them back at the start of every session. Everything is plain text inside your project, and it travels with the code — `git clone` brings the memory along.
|
|
31
|
+
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> **Not a developer?** If you can open a project in Claude Code, you're set — let Claude run the setup for you (see [Quickstart](#quickstart)).
|
|
9
34
|
|
|
10
|
-
|
|
35
|
+
## How it feels
|
|
11
36
|
|
|
12
|
-
|
|
37
|
+
You open Claude Code on a project you haven't touched in weeks. Before you say anything, Claude already knows your stack, your conventions, and what you decided last time:
|
|
13
38
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **Don't start empty — import the rules you already own** — `cmk import-claude-md` parses an existing `CLAUDE.md` / `.cursorrules` / `AGENTS.md` into typed, searchable facts through the same safe write path (secret screening, sanitization, dedup), with provenance back to source file + line. `--dry-run` previews first.
|
|
22
|
-
- **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
|
|
23
|
-
- **9 health checks** — `cmk doctor` validates hook wiring, distill freshness, transcript firing, INDEX consistency, cron registration, native-memory coexistence, stale locks, native-binding health (npm 12 readiness), and version drift (a project scaffold behind your installed `cmk` after an update) — each failure with a repair command.
|
|
39
|
+
```
|
|
40
|
+
claude-memory-kit: 23 fact(s) in context, 2 captured in the last 24h, 1 conflict pending
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You work. It learns — automatically, no buttons. Next session, it remembers this one too.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
- **Remembers across sessions** — a frozen snapshot of your project + persona injects once at session start, so Claude leads with what it knows instead of re-deriving it from code.
|
|
48
|
+
- **Captures automatically, prompt-free** — a background pass reads each turn and saves durable facts as searchable notes. No "save" button. When you *do* say "remember this," the kit auto-approves its **own** tools and skills so the save happens with no "Allow?" prompt — nothing else is touched.
|
|
49
|
+
- **Recalls by meaning** — ask in your own words ("where do credentials go") and get the right fact even with zero keyword overlap. Fully local, zero API calls — **R@5 0.941 / paraphrase 1.000** ([benchmarks](#benchmarks)).
|
|
50
|
+
- **Learns how you work, everywhere** — state a habit once ("always use uv, never pip") and a brand-new project cold-opens already knowing it.
|
|
51
|
+
- **Stays private + bounded** — secrets are screened before any write, machine paths are abstracted to `~`, and rolling compression keeps memory small as history grows.
|
|
52
|
+
- **Guards against accidental deletion** — a hook **blocks** a destructive command (`rm`, `git reset --hard`, …) the moment it targets a memory path, before it runs.
|
|
53
|
+
- **Works across your agents** — the same memory brain on **Claude Code** and **[Kiro](https://kiro.dev)** (IDE + `kiro-cli`). A project's `context/` is shared, so memory you build in one is there in the other.
|
|
54
|
+
- **Per-project, in your repo** — `context/` lives in your project and travels with `git clone`. Each project keeps its own memory.
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
> [!IMPORTANT]
|
|
59
|
+
> Pick **one** route — both wire the same hooks and are complete on their own.
|
|
28
60
|
|
|
29
61
|
### Route A — npm (recommended)
|
|
30
62
|
|
|
63
|
+
Install the CLI once, then run `cmk install` in each project — pick your agent:
|
|
64
|
+
|
|
31
65
|
```bash
|
|
32
66
|
npm install -g @lh8ppl/claude-memory-kit
|
|
33
67
|
cd ~/my-project
|
|
34
|
-
cmk install # scaffolds context/ + the memory-write + memory-search skills AND wires the lifecycle hooks into .claude/settings.json
|
|
35
|
-
cmk install --with-semantic # (optional) local semantic recall — one-time ~260 MB, search defaults to hybrid
|
|
36
|
-
cmk register-crons # (optional) scheduled background compression — otherwise self-heals lazily
|
|
37
|
-
cmk import-claude-md --yes # (optional) seed memory from an existing CLAUDE.md / .cursorrules (--dry-run previews)
|
|
38
|
-
cmk doctor # verify, then restart Claude Code
|
|
39
68
|
```
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
**Claude Code:**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cmk install # scaffold context/ + wire hooks (one step)
|
|
74
|
+
cmk install --with-semantic # optional: local semantic recall (~260 MB, once)
|
|
75
|
+
cmk doctor # verify, then restart Claude Code
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Kiro** (IDE + `kiro-cli` — see [the Kiro guide](https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/KIRO.md)):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cmk install --ide kiro # wire Kiro end-to-end
|
|
82
|
+
cmk install --ide kiro --with-semantic # …with local semantic recall
|
|
83
|
+
cmk doctor # verify, then restart Kiro
|
|
84
|
+
```
|
|
42
85
|
|
|
43
|
-
|
|
86
|
+
`cmk install` is the whole entry point: it scaffolds `context/`, drops the memory skills, wires the lifecycle hooks, and registers the MCP server so the agent can drive memory as tools — no `/plugin` step needed. A project can carry **both** agents — run both installs; they share one `context/`.
|
|
44
87
|
|
|
45
|
-
|
|
88
|
+
> [!TIP]
|
|
89
|
+
> Prefer not to touch the terminal? Open the project in Claude Code and say *"install claude-memory-kit and set it up here."* Claude runs the commands; you just approve them. **Restart Claude Code once** afterward (`/exit`, then `claude`) so the hooks load.
|
|
46
90
|
|
|
47
|
-
|
|
91
|
+
### Route B — Claude Code plugin
|
|
48
92
|
|
|
49
93
|
```text
|
|
50
94
|
/plugin marketplace add LH8PPL/claude-memory-kit # add this repo as a plugin source (once per machine)
|
|
51
|
-
/plugin install claude-memory-kit # install
|
|
52
|
-
cd ~/my-project
|
|
53
|
-
/claude-memory-kit:bootstrap # scaffold this project's
|
|
95
|
+
/plugin install claude-memory-kit # install hooks + skills (once per machine)
|
|
96
|
+
cd ~/my-project
|
|
97
|
+
/claude-memory-kit:bootstrap # scaffold this project's memory (once per project)
|
|
54
98
|
```
|
|
55
99
|
|
|
56
|
-
The
|
|
100
|
+
The plugin bundles the hooks + skills, so it's complete without the npm CLI. Add the CLI later only if you want `cmk search` / `cmk doctor` / cron.
|
|
101
|
+
|
|
102
|
+
> [!NOTE]
|
|
103
|
+
> **Updating** has two parts on both routes: update the machinery, then re-stamp each project (`cmk install` again, or `/claude-memory-kit:bootstrap`). `cmk doctor` flags any project that's behind so you don't have to remember.
|
|
104
|
+
|
|
105
|
+
## How it works
|
|
106
|
+
|
|
107
|
+
`context/` is the source of truth — plain markdown, committed with your code. A regenerable SQLite + FTS5 index (plus an optional local embedder) powers search. Memory lives in three tiers:
|
|
108
|
+
|
|
109
|
+
| Tier | Location | Scope | What lives here |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| **Project** | `<repo>/context/` | committed — travels with `clone` | Decisions, conventions, file purposes |
|
|
112
|
+
| **Local** | `<repo>/context.local/` | gitignored, per-machine | Machine paths, local tool versions |
|
|
113
|
+
| **User** | `~/.claude-memory-kit/` | cross-project, per-person | Persona, cross-project lessons |
|
|
114
|
+
|
|
115
|
+
Project memory follows the **repo** (teammates get it on clone). Your persona follows **you** — machine-local, never committed. Carry it between your own machines with `cmk persona export` / `import`.
|
|
57
116
|
|
|
58
117
|
## CLI
|
|
59
118
|
|
|
60
|
-
|
|
119
|
+
You rarely type these yourself — Claude drives the same operations as tools mid-conversation through the kit's **MCP server** (full tool reference: **[docs/MCP.md](https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/MCP.md)**). The commands:
|
|
61
120
|
|
|
62
121
|
| Command | Purpose |
|
|
63
122
|
| --- | --- |
|
|
64
|
-
| `cmk install` | Scaffold
|
|
65
|
-
| `cmk
|
|
123
|
+
| `cmk install [--with-semantic] [--ide claude-code\|kiro]` | Scaffold + wire hooks + register the MCP server (complete entry point) |
|
|
124
|
+
| `cmk uninstall [--ide claude-code\|kiro]` | Remove one agent's wiring — conservative, never deletes `context/` |
|
|
125
|
+
| `cmk doctor` | Run HC-1..HC-10 health checks; surface a repair command per failure |
|
|
66
126
|
| `cmk repair --hooks` / `--locks` / `--index` / `--all` | Idempotent self-repair |
|
|
67
|
-
| `cmk search "<query>" [--mode keyword\|semantic\|hybrid] [--scope facts\|transcripts\|decisions]` | Search memory
|
|
68
|
-
| `cmk
|
|
127
|
+
| `cmk search "<query>" [--mode keyword\|semantic\|hybrid] [--scope facts\|transcripts\|decisions]` | Search memory by meaning (hybrid default after `--with-semantic`); `--scope transcripts` = raw session record; `--scope decisions` = the decision journal (history / "what did we reject") |
|
|
128
|
+
| `cmk remember "<fact>"` | Capture a fact explicitly (deduped, secret-screened, path-abstracted). `--from-file fact.json` for backtick/quote-heavy rich facts |
|
|
129
|
+
| `cmk get <id…>` / `cmk timeline <id>` / `cmk cite <id>` / `cmk recent-activity` | Read the index back — full fact bodies + provenance, context around an observation, a citation link, recent changes (the CLI side of the `mk_*` MCP read tools) |
|
|
130
|
+
| `cmk forget <id>` | Tombstone a fact — gone from `cmk search` immediately (audit trail preserved) |
|
|
131
|
+
| `cmk lessons promote <id> [--to USER.md\|HABITS.md]` | Promote one project fact to your cross-project **user tier** so it applies in **every** project |
|
|
69
132
|
| `cmk roll --scope now\|today\|recent` | Manually trigger a compression pipeline |
|
|
70
|
-
| `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly jobs
|
|
71
|
-
| `cmk
|
|
72
|
-
| `cmk
|
|
73
|
-
| `cmk
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
| `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly compression jobs (cron / launchd / Task Scheduler) |
|
|
134
|
+
| `cmk disable-native-memory` / `enable-native-memory` | Opt out of Claude Code's built-in Auto Memory so the kit is your single, lean layer |
|
|
135
|
+
| `cmk persona generate` · `export <file>` · `import <file>` | Synthesize your cross-project persona on demand; carry it to another of **your** machines (private — never committed) |
|
|
136
|
+
| `cmk import-claude-md [file]` · `import-anthropic-memory` | Seed memory from an existing `CLAUDE.md` / `.cursorrules` / `AGENTS.md`, or merge Anthropic's native auto-memory bullets (`--dry-run` previews) |
|
|
137
|
+
|
|
138
|
+
Full reference with examples: **[docs/CLI.md](https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/CLI.md)** or `cmk --help`.
|
|
139
|
+
|
|
140
|
+
## Working with Kiro
|
|
141
|
+
|
|
142
|
+
[Kiro](https://kiro.dev) (the AWS agentic IDE + `kiro-cli`) is a first-class target — `cmk install --ide kiro` wires it end-to-end for both the IDE and the terminal, and a project's `context/` is shared with Claude Code. The full setup, surface table, and dual-agent notes are in **[the Kiro guide](https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/KIRO.md)**.
|
|
143
|
+
|
|
144
|
+
## Uninstalling
|
|
145
|
+
|
|
146
|
+
`cmk uninstall` is **conservative** — it removes only the kit's managed wiring for one agent and **never deletes your `context/` memory** (your data) or anything outside the kit's markers.
|
|
147
|
+
|
|
148
|
+
**Claude Code:**
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
cmk uninstall # remove the CLAUDE.md block + hooks
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Kiro:**
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cmk uninstall --ide kiro # remove the .kiro/ blocks + skills + IDE hooks + AGENTS.md + ~/.kiro CLI agent
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
On a dual-agent project, uninstall one and the other keeps working. To remove the memory data too, delete `context/` (and `context.local/`) yourself — the kit won't do it for you.
|
|
161
|
+
|
|
162
|
+
## Benchmarks
|
|
163
|
+
|
|
164
|
+
Recall quality is **measured, not claimed** — `npm run bench:recall` runs a LongMemEval-style harness through the kit's real write / index / search paths.
|
|
165
|
+
|
|
166
|
+
| Pipeline | R@5 | Paraphrase recall | API calls |
|
|
167
|
+
| --- | --- | --- | --- |
|
|
168
|
+
| Keyword (FTS5 one-shot) | 0.176 | 0.000 | 0 |
|
|
169
|
+
| Agentic keyword (iterative + LLM reformulation) | 0.529 | 0.300 | 1/query |
|
|
170
|
+
| **Semantic (sqlite-vec + local bge-base, the default)** | **0.941** | **1.000** | **0** |
|
|
171
|
+
|
|
172
|
+
Keyword search structurally misses natural-language questions; the embedded semantic backend closes the paraphrase gap entirely — locally, with no API calls.
|
|
78
173
|
|
|
79
174
|
## Requirements
|
|
80
175
|
|
|
81
176
|
- Node.js ≥ 20
|
|
82
|
-
- Claude Code (for the hook-driven auto-memory loop)
|
|
177
|
+
- Claude Code (for the hook-driven auto-memory loop) — or [Kiro](https://kiro.dev)
|
|
83
178
|
- Optional: `cmk install --with-semantic` for semantic/hybrid recall (installs the local `@huggingface/transformers` embedder, ~260 MB once — no API, no Python)
|
|
84
179
|
|
|
85
|
-
## Three-tier model
|
|
86
|
-
|
|
87
|
-
| Tier | Location | Scope |
|
|
88
|
-
| --- | --- | --- |
|
|
89
|
-
| **P** (project) | `<repo>/context/` | committed to git, travels with `clone` |
|
|
90
|
-
| **L** (local) | `<repo>/context.local/` | gitignored, per-machine |
|
|
91
|
-
| **U** (user) | `~/.claude-memory-kit/` | cross-project per-user |
|
|
92
|
-
|
|
93
180
|
## Documentation
|
|
94
181
|
|
|
95
182
|
Full docs, architecture, and design live in the repository:
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PermissionRequest hook handler — the prompt-free auto-approver (Task 172).
|
|
3
|
+
//
|
|
4
|
+
// Wired by `cmk install` as a Claude Code PermissionRequest hook (matchers
|
|
5
|
+
// "mcp__cmk__.*" and "Skill"). Reads the permission request on stdin and, if it
|
|
6
|
+
// is for one of the kit's OWN surfaces (an `mcp__cmk__<tool>` MCP tool or a
|
|
7
|
+
// scaffolded kit skill), prints the allow decision to stdout so Claude Code
|
|
8
|
+
// approves it without a prompt. For anything else it prints nothing and exits 0,
|
|
9
|
+
// leaving CC's normal permission flow untouched.
|
|
10
|
+
//
|
|
11
|
+
// Why this exists: CC 2.1.x stopped honouring the kit's `permissions.allow`
|
|
12
|
+
// MCP rules + skill `allowed-tools` for these prompts (anthropics/claude-code
|
|
13
|
+
// #17499, #18837→#14956). The PermissionRequest hook is the documented,
|
|
14
|
+
// working mechanism (proven live, v041l).
|
|
15
|
+
//
|
|
16
|
+
// Output contract (code.claude.com/docs/en/hooks-guide):
|
|
17
|
+
// stdout = {"hookSpecificOutput":{"hookEventName":"PermissionRequest",
|
|
18
|
+
// "decision":{"behavior":"allow"}}} → auto-approve
|
|
19
|
+
// stdout empty + exit 0 → no opinion (CC asks as usual)
|
|
20
|
+
// Fail-SILENT: any load/parse/logic error prints nothing and exits 0 — a broken
|
|
21
|
+
// auto-approver must never wedge the session OR wrongly approve; it just stops
|
|
22
|
+
// auto-approving and the user sees the normal prompt.
|
|
23
|
+
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
30
|
+
const modulePath = join(__dirname, '..', 'src', 'approve-permission.mjs');
|
|
31
|
+
|
|
32
|
+
let readHookStdin;
|
|
33
|
+
let evaluatePermissionRequest;
|
|
34
|
+
try {
|
|
35
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
36
|
+
({ evaluatePermissionRequest } = await import(pathToFileURL(modulePath).href));
|
|
37
|
+
} catch {
|
|
38
|
+
process.exit(0); // fail-silent: no opinion
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Drain the hook payload — but not on an interactive TTY (a manual run), where a
|
|
42
|
+
// blocking stdin read would hang forever (the Task-101 lesson).
|
|
43
|
+
const raw = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
44
|
+
|
|
45
|
+
let payload;
|
|
46
|
+
try {
|
|
47
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
process.exit(0); // fail-silent on unparseable input
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let decision;
|
|
53
|
+
try {
|
|
54
|
+
decision = evaluatePermissionRequest(payload);
|
|
55
|
+
} catch {
|
|
56
|
+
process.exit(0); // fail-silent on any logic error
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (decision) {
|
|
60
|
+
process.stdout.write(JSON.stringify(decision));
|
|
61
|
+
}
|
|
62
|
+
process.exit(0);
|
|
@@ -26,12 +26,15 @@ const __dirname = dirname(__filename);
|
|
|
26
26
|
// relative to bin/ → ../src/ for both modules.
|
|
27
27
|
const dailyDistillModulePath = join(__dirname, '..', 'src', 'daily-distill.mjs');
|
|
28
28
|
const compressorModulePath = join(__dirname, '..', 'src', 'compressor.mjs');
|
|
29
|
+
const compactionStateModulePath = join(__dirname, '..', 'src', 'compaction-state.mjs');
|
|
29
30
|
|
|
30
31
|
let dailyDistill;
|
|
31
32
|
let HaikuViaAnthropicApi;
|
|
33
|
+
let recordCronHeartbeat;
|
|
32
34
|
try {
|
|
33
35
|
({ dailyDistill } = await import(pathToFileURL(dailyDistillModulePath).href));
|
|
34
36
|
({ HaikuViaAnthropicApi } = await import(pathToFileURL(compressorModulePath).href));
|
|
37
|
+
({ recordCronHeartbeat } = await import(pathToFileURL(compactionStateModulePath).href));
|
|
35
38
|
} catch (err) {
|
|
36
39
|
process.stderr.write(
|
|
37
40
|
`cmk-daily-distill: failed to load modules: ${err?.message ?? err}\n`,
|
|
@@ -52,6 +55,17 @@ const envRoot = process.env.CMK_PROJECT_DIR && process.env.CMK_PROJECT_DIR.lengt
|
|
|
52
55
|
: null;
|
|
53
56
|
const projectRoot = argvRoot ?? envRoot ?? process.cwd();
|
|
54
57
|
|
|
58
|
+
// Task 167 (D-207): record the cron HEARTBEAT on every fire — BEFORE the distill
|
|
59
|
+
// work, so a run that crashes mid-distill still proves "the cron is alive" (the
|
|
60
|
+
// anacron model: a run HAPPENED, regardless of outcome). This is the durable
|
|
61
|
+
// liveness signal the lazy-roll gate keys off (by age); without it a registered
|
|
62
|
+
// cron would read "dead" after 48h even while firing nightly. Best-effort.
|
|
63
|
+
try {
|
|
64
|
+
recordCronHeartbeat?.({ projectRoot });
|
|
65
|
+
} catch {
|
|
66
|
+
// never let a heartbeat write failure abort the distill
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
try {
|
|
56
70
|
const backend = new HaikuViaAnthropicApi();
|
|
57
71
|
const r = await dailyDistill({ projectRoot, backend });
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook handler — the memory delete-guardrail (D-192).
|
|
3
|
+
//
|
|
4
|
+
// Wired by `cmk install` as a Claude Code PreToolUse hook (matcher
|
|
5
|
+
// "Bash|PowerShell"). Reads the tool call on stdin and, if it's a destructive
|
|
6
|
+
// command aimed at a claude-memory-kit memory path (context/ , the persona
|
|
7
|
+
// tier, a memory file), BLOCKS it by exiting 2 — Claude Code shows the stderr
|
|
8
|
+
// reason to the model and the command never runs.
|
|
9
|
+
//
|
|
10
|
+
// Exit contract (Claude Code PreToolUse):
|
|
11
|
+
// exit 0 → allow (no opinion)
|
|
12
|
+
// exit 2 → BLOCK; stderr is surfaced as the reason
|
|
13
|
+
// Fail-OPEN: any load/parse error exits 0 — a broken guardrail must never wedge
|
|
14
|
+
// the session; it just stops guarding.
|
|
15
|
+
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
22
|
+
const modulePath = join(__dirname, '..', 'src', 'guard-memory.mjs');
|
|
23
|
+
|
|
24
|
+
let readHookStdin;
|
|
25
|
+
let evaluatePayload;
|
|
26
|
+
try {
|
|
27
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
28
|
+
({ evaluatePayload } = await import(pathToFileURL(modulePath).href));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
process.stderr.write(`cmk-guard-memory: failed to load modules: ${err?.message ?? err}\n`);
|
|
31
|
+
process.exit(0); // fail-open
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Drain the hook payload — but not on an interactive TTY (a manual run), where
|
|
35
|
+
// a blocking stdin read would hang forever (the Task-101 lesson).
|
|
36
|
+
const raw = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
37
|
+
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
process.exit(0); // fail-open on unparseable input
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let verdict;
|
|
46
|
+
try {
|
|
47
|
+
verdict = evaluatePayload(payload);
|
|
48
|
+
} catch {
|
|
49
|
+
process.exit(0); // fail-open on any logic error
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (verdict && verdict.block) {
|
|
53
|
+
process.stderr.write(`${verdict.reason}\n`);
|
|
54
|
+
process.exit(2); // BLOCK
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.exit(0); // allow
|
|
@@ -59,6 +59,18 @@ try {
|
|
|
59
59
|
// is discarded; readHookStdin returns '' for a TTY so a manual run finishes.
|
|
60
60
|
readHookStdin({ isTTY: process.stdin.isTTY });
|
|
61
61
|
|
|
62
|
+
// Task 167 NOTE (D-207, the live-test revision): an earlier design (Q4) tried a
|
|
63
|
+
// SYNCHRONOUS now.md drain HERE, before injectContext, to make THIS session read
|
|
64
|
+
// the rolled state. The live test proved that doesn't fit: a real now→today Haiku
|
|
65
|
+
// roll takes ~18–37s, but the SessionStart hook ceiling is 30s — so the
|
|
66
|
+
// synchronous drain reliably timed out and fell back to the detached path anyway.
|
|
67
|
+
// The research (claude-mem/mem0/Letta) confirms the event-driven peers compact at
|
|
68
|
+
// session END (the Stop hook, no user waiting), NOT session start. So the kit
|
|
69
|
+
// keeps the now→today roll on the DETACHED SessionStart path (spawned inside
|
|
70
|
+
// injectContext) + the SessionEnd compress-session hook — and the real fix is the
|
|
71
|
+
// cron-liveness gate (167.A), which stops a dead cron from suppressing that roll.
|
|
72
|
+
// now.md heals next session via the detached roll and never compounds.
|
|
73
|
+
|
|
62
74
|
try {
|
|
63
75
|
const r = injectContext({ cwd: process.cwd(), compressLazyPath });
|
|
64
76
|
process.stdout.write(JSON.stringify(r.hookOutput));
|
|
@@ -20,12 +20,15 @@ const __dirname = dirname(__filename);
|
|
|
20
20
|
|
|
21
21
|
const weeklyCurateModulePath = join(__dirname, '..', 'src', 'weekly-curate.mjs');
|
|
22
22
|
const compressorModulePath = join(__dirname, '..', 'src', 'compressor.mjs');
|
|
23
|
+
const compactionStateModulePath = join(__dirname, '..', 'src', 'compaction-state.mjs');
|
|
23
24
|
|
|
24
25
|
let weeklyCurate;
|
|
25
26
|
let HaikuViaAnthropicApi;
|
|
27
|
+
let recordCronHeartbeat;
|
|
26
28
|
try {
|
|
27
29
|
({ weeklyCurate } = await import(pathToFileURL(weeklyCurateModulePath).href));
|
|
28
30
|
({ HaikuViaAnthropicApi } = await import(pathToFileURL(compressorModulePath).href));
|
|
31
|
+
({ recordCronHeartbeat } = await import(pathToFileURL(compactionStateModulePath).href));
|
|
29
32
|
} catch (err) {
|
|
30
33
|
process.stderr.write(
|
|
31
34
|
`cmk-weekly-curate: failed to load modules: ${err?.message ?? err}\n`,
|
|
@@ -49,6 +52,15 @@ const projectRoot = argvRoot ?? envRoot ?? process.cwd();
|
|
|
49
52
|
// project-only (backward-compatible).
|
|
50
53
|
const userDir = join(homedir(), '.claude-memory-kit');
|
|
51
54
|
|
|
55
|
+
// Task 167 (D-207): record the cron heartbeat on every fire (anacron model —
|
|
56
|
+
// proves the cron is alive regardless of curate outcome). The lazy-roll gate
|
|
57
|
+
// keys off its age. Best-effort; never abort the curate on a heartbeat failure.
|
|
58
|
+
try {
|
|
59
|
+
recordCronHeartbeat?.({ projectRoot });
|
|
60
|
+
} catch {
|
|
61
|
+
// best-effort
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
try {
|
|
53
65
|
const backend = new HaikuViaAnthropicApi();
|
|
54
66
|
const r = await weeklyCurate({ projectRoot, userDir, backend });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lh8ppl/claude-memory-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "cmk — the CLI for claude-memory-kit. Per-project, in-repo memory system for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"cmk-capture-prompt": "./bin/cmk-capture-prompt.mjs",
|
|
13
13
|
"cmk-observe-edit": "./bin/cmk-observe-edit.mjs",
|
|
14
14
|
"cmk-capture-turn": "./bin/cmk-capture-turn.mjs",
|
|
15
|
-
"cmk-compress-session": "./bin/cmk-compress-session.mjs"
|
|
15
|
+
"cmk-compress-session": "./bin/cmk-compress-session.mjs",
|
|
16
|
+
"cmk-guard-memory": "./bin/cmk-guard-memory.mjs",
|
|
17
|
+
"cmk-approve-permission": "./bin/cmk-approve-permission.mjs"
|
|
16
18
|
},
|
|
17
19
|
"files": [
|
|
18
20
|
"bin/",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// agent-profile.mjs — the per-agent profile factory (Task 50.C).
|
|
2
|
+
//
|
|
3
|
+
// D-180: per-agent adapters are DATA, not classes. This factory validates +
|
|
4
|
+
// normalizes a profile DECLARATION so the install routing (Task 50.F) can drive
|
|
5
|
+
// ANY agent through ONE code path — the config legs (MCP registration, hook
|
|
6
|
+
// entry) go through the shared `mutateAgentConfig` primitive (Task 50.B); the
|
|
7
|
+
// instruction leg goes through the kit's marker-block machinery. The factory
|
|
8
|
+
// itself does NO I/O — it's a pure validator/normalizer that returns a frozen
|
|
9
|
+
// descriptor. A bad profile fails LOUD at definition time (throws), not at
|
|
10
|
+
// install time against a user's machine.
|
|
11
|
+
//
|
|
12
|
+
// Integration-type taxonomy (the claude-mem insight — the type dictates which
|
|
13
|
+
// legs an agent wires):
|
|
14
|
+
// native-hooks-mcp — instruction + MCP + lifecycle hooks (Claude Code, Kiro)
|
|
15
|
+
// hooks-mcp — instruction + MCP + hooks, hooks via a dedicated file
|
|
16
|
+
// (Cursor) — same required legs as native-hooks-mcp;
|
|
17
|
+
// the `hooks.mechanism` field distinguishes them
|
|
18
|
+
// mcp-only — instruction + MCP, NO hooks (Copilot/Warp/Roo/Goose)
|
|
19
|
+
// instruction-only — instruction file only, NO MCP, NO hooks (AGENTS.md rung)
|
|
20
|
+
//
|
|
21
|
+
// Public surface:
|
|
22
|
+
// defineAgentProfile(declaration) → frozen normalized descriptor (throws on invalid)
|
|
23
|
+
// INTEGRATION_TYPES — frozen list of the valid integrationType values
|
|
24
|
+
|
|
25
|
+
export const INTEGRATION_TYPES = Object.freeze([
|
|
26
|
+
'native-hooks-mcp',
|
|
27
|
+
'hooks-mcp',
|
|
28
|
+
'mcp-only',
|
|
29
|
+
'instruction-only',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const TYPES = new Set(INTEGRATION_TYPES);
|
|
33
|
+
|
|
34
|
+
// Which legs each type REQUIRES / FORBIDS. instructionFile is required by every
|
|
35
|
+
// type (the universal leg). mcp + hooks vary by type.
|
|
36
|
+
const TYPE_LEGS = Object.freeze({
|
|
37
|
+
'native-hooks-mcp': { mcp: 'required', hooks: 'required' },
|
|
38
|
+
'hooks-mcp': { mcp: 'required', hooks: 'required' },
|
|
39
|
+
'mcp-only': { mcp: 'required', hooks: 'forbidden' },
|
|
40
|
+
'instruction-only': { mcp: 'forbidden', hooks: 'forbidden' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function fail(msg) {
|
|
44
|
+
throw new Error(`defineAgentProfile: ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate + normalize an agent profile declaration.
|
|
49
|
+
* @param {object} decl
|
|
50
|
+
* @returns {Readonly<object>} the frozen descriptor
|
|
51
|
+
*/
|
|
52
|
+
export function defineAgentProfile(decl) {
|
|
53
|
+
if (decl === null || typeof decl !== 'object') {
|
|
54
|
+
fail('declaration must be an object');
|
|
55
|
+
}
|
|
56
|
+
const { name, displayName, integrationType, detect, instructionFile, mcp, hooks, transcript } = decl;
|
|
57
|
+
|
|
58
|
+
// ── universal required fields ────────────────────────────────────────────
|
|
59
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
60
|
+
fail('name is required (non-empty string)');
|
|
61
|
+
}
|
|
62
|
+
if (!TYPES.has(integrationType)) {
|
|
63
|
+
fail(`integrationType must be one of: ${INTEGRATION_TYPES.join(', ')} (got ${JSON.stringify(integrationType)})`);
|
|
64
|
+
}
|
|
65
|
+
if (detect === null || typeof detect !== 'object') {
|
|
66
|
+
fail(`profile ${name}: detect descriptor is required (e.g. {homeDir:'.kiro'} | {command:'x'} | {always:true})`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof instructionFile !== 'string' || instructionFile.length === 0) {
|
|
69
|
+
fail(`profile ${name}: instructionFile is required (every integration type wires the instruction leg)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── per-type leg contract (the parity invariant 50.D will also enforce) ──
|
|
73
|
+
const legs = TYPE_LEGS[integrationType];
|
|
74
|
+
enforceLeg(name, integrationType, 'mcp', mcp, legs.mcp);
|
|
75
|
+
enforceLeg(name, integrationType, 'hooks', hooks, legs.hooks);
|
|
76
|
+
|
|
77
|
+
// ── shape checks on present legs ─────────────────────────────────────────
|
|
78
|
+
if (mcp !== undefined) {
|
|
79
|
+
if (typeof mcp.path !== 'string' || typeof mcp.serversKey !== 'string') {
|
|
80
|
+
fail(`profile ${name}: mcp requires {path, serversKey} strings`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (hooks !== undefined) {
|
|
84
|
+
if (typeof hooks.mechanism !== 'string') {
|
|
85
|
+
fail(`profile ${name}: hooks requires a {mechanism} string`);
|
|
86
|
+
}
|
|
87
|
+
if (hooks.eventMap === null || typeof hooks.eventMap !== 'object') {
|
|
88
|
+
fail(`profile ${name}: hooks requires an {eventMap} object`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── normalize + freeze ───────────────────────────────────────────────────
|
|
93
|
+
const descriptor = {
|
|
94
|
+
name,
|
|
95
|
+
displayName: displayName || name,
|
|
96
|
+
integrationType,
|
|
97
|
+
detect: Object.freeze({ ...detect }),
|
|
98
|
+
instructionFile,
|
|
99
|
+
...(mcp !== undefined ? { mcp: Object.freeze({ ...mcp }) } : {}),
|
|
100
|
+
...(hooks !== undefined
|
|
101
|
+
? { hooks: Object.freeze({ ...hooks, eventMap: Object.freeze({ ...hooks.eventMap }) }) }
|
|
102
|
+
: {}),
|
|
103
|
+
...(transcript !== undefined ? { transcript: Object.freeze({ ...transcript }) } : {}),
|
|
104
|
+
};
|
|
105
|
+
return Object.freeze(descriptor);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enforceLeg(name, type, leg, value, requirement) {
|
|
109
|
+
if (requirement === 'required' && value === undefined) {
|
|
110
|
+
fail(`profile ${name}: integrationType '${type}' requires the ${leg} leg`);
|
|
111
|
+
}
|
|
112
|
+
if (requirement === 'forbidden' && value !== undefined) {
|
|
113
|
+
fail(`profile ${name}: integrationType '${type}' must NOT declare ${leg} (it over-wires its type)`);
|
|
114
|
+
}
|
|
115
|
+
}
|