@lh8ppl/claude-memory-kit 0.4.0 → 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 CHANGED
@@ -5,97 +5,178 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- # @lh8ppl/claude-memory-kit
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>
9
11
 
10
- **`cmk`** — the CLI for [claude-memory-kit](https://github.com/LH8PPL/claude-memory-kit), a per-project, in-repo memory system for [Claude Code](https://docs.claude.com/en/docs/claude-code). It fixes Claude's per-session amnesia so you don't have to re-tell the backstory every time you start a new session.
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>
11
18
 
12
- ## What it does
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>
13
24
 
14
- - **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`.
15
- - **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. The snapshot opens with an **authority instruction** ("when injected memory contradicts your assumptions, injected memory wins"), so the agent leads with its memory instead of re-deriving answers from the code.
16
- - **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.
17
- - **Claude knows WHEN to recall** — the auto-invoked `memory-search` skill fires on "what did we decide about X" / "have we seen this error before" and searches the deep archive in a forked side-context, returning a curated citation-backed summary. Read-only by contract.
18
- - **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.
19
- - **Search + MCP — Claude runs every memory op for you, in conversation** `cmk search "<term>"` (keyword over facts + scratchpads; with the optional local embedder, **semantic + hybrid recall**: ask in your own words and get the fact even with zero keyword overlap — measured R@5 0.941 / paraphrase 1.000 on the kit's benchmark, no API calls). `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.
20
- - **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.
21
- - **Guards your memory from an accidental delete** — `cmk install` wires a `PreToolUse` hook (`cmk-guard-memory`) that **blocks** a destructive command (`rm`, `Remove-Item`, `del`, `git clean`, `git reset --hard`, `find … -delete`, `truncate`, `>`-truncate) the moment it's aimed at a memory path — *before* it runs, on both Claude Code (`Bash` / `PowerShell`) and Kiro (`execute_bash`). Fail-open (a broken guard never wedges your session) and intentionally broad (a false block is recoverable; a false allow is the data loss it prevents). A safe command, or a delete of anything else, runs untouched.
22
- - **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.
23
- - **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
24
- - **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.
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 endsso 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)).
25
34
 
26
- ## Install pick ONE route
35
+ ## How it feels
27
36
 
28
- Each route is complete on its own. **Don't run both** they wire the same hooks.
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:
38
+
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
46
+
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.
55
+
56
+ ## Quickstart
57
+
58
+ > [!IMPORTANT]
59
+ > Pick **one** route — both wire the same hooks and are complete on their own.
29
60
 
30
61
  ### Route A — npm (recommended)
31
62
 
63
+ Install the CLI once, then run `cmk install` in each project — pick your agent:
64
+
32
65
  ```bash
33
66
  npm install -g @lh8ppl/claude-memory-kit
34
67
  cd ~/my-project
35
- cmk install # scaffolds context/ + the memory-write + memory-search skills AND wires the lifecycle hooks into .claude/settings.json
36
- cmk install --with-semantic # (optional) local semantic recall — one-time ~260 MB, search defaults to hybrid
37
- cmk install --ide kiro # (optional) target Kiro instead — wires the IDE + kiro-cli surfaces (MCP + steering + AGENTS.md + skills + hooks)
38
- cmk register-crons # (optional) scheduled background compression — otherwise self-heals lazily
39
- cmk import-claude-md --yes # (optional) seed memory from an existing CLAUDE.md / .cursorrules (--dry-run previews)
40
- cmk doctor # verify, then restart Claude Code
41
68
  ```
42
69
 
43
- `cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` + `memory-search` skills 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, and writes a `.gitattributes` block pinning committed memory to LF (so a Windows clone can't mangle line endings — your memory stays readable cross-platform). No separate `/plugin` step needed. Use `cmk install --no-hooks` to skip the hooks + MCP wiring (scaffold-only).
70
+ **Claude Code:**
44
71
 
45
- > 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`.
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)):
46
79
 
47
- **Other agents (Kiro).** `cmk install --ide kiro` targets [Kiro](https://kiro.dev) (AWS's agentic IDE + `kiro-cli`) instead of Claude Code — one command wires both its GUI hooks (`.kiro/hooks/`) and its terminal CLI agent (`~/.aws/amazonq/cli-agents/`), plus MCP, steering, skills, and an `AGENTS.md` instruction file. A project can carry **both** agents (run both installs — they share one `context/` brain and never clobber each other). Restart Kiro to load its hooks.
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
+ ```
48
85
 
49
- **Uninstall** is per-agent and conservative it never deletes your `context/` memory: `cmk uninstall` removes the Claude Code surface; `cmk uninstall --ide kiro` removes the Kiro surface.
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/`.
50
87
 
51
- ### Route B — Claude Code plugin marketplace
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.
52
90
 
53
- Inside Claude Code:
91
+ ### Route B — Claude Code plugin
54
92
 
55
93
  ```text
56
94
  /plugin marketplace add LH8PPL/claude-memory-kit # add this repo as a plugin source (once per machine)
57
- /plugin install claude-memory-kit # install the global machinery — hooks + skills (once per machine)
58
- cd ~/my-project # the project you want memory in — bootstrap scaffolds into the CURRENT dir
59
- /claude-memory-kit:bootstrap # scaffold this project's context/ memory tree (once per project)
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)
60
98
  ```
61
99
 
62
- The first two commands are **global** (per machine); `bootstrap` is **per project** — run it again (after a `cd`) in each project. The plugin bundles the hooks + the `bootstrap`, `memory-write`, and `memory-search` skills, so it's complete without the npm CLI (add the CLI later only if you want `cmk search` / `cmk doctor` / cron).
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`.
63
116
 
64
117
  ## CLI
65
118
 
66
- Most-used commands (full list via `cmk --help`):
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:
67
120
 
68
121
  | Command | Purpose |
69
122
  | --- | --- |
70
- | `cmk install` | Scaffold `context/` + the `memory-write`/`memory-search` skills + `.gitignore` + CLAUDE.md block + wire hooks (`--no-hooks` for scaffold-only) |
71
- | `cmk doctor` | Run HC-1..HC-9 health checks, surface repair commands |
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 |
72
126
  | `cmk repair --hooks` / `--locks` / `--index` / `--all` | Idempotent self-repair |
73
- | `cmk search "<query>" [--mode keyword\|semantic\|hybrid] [--scope facts\|transcripts\|decisions]` | Search memory by meaning with the embedder (hybrid default after `--with-semantic`); `--scope transcripts` = the raw session record; `--scope decisions` = the decision journal (history / "what did we reject") |
74
- | `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) |
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 |
75
132
  | `cmk roll --scope now\|today\|recent` | Manually trigger a compression pipeline |
76
- | `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly jobs with cron / launchd / Task Scheduler |
77
- | `cmk forget <id>` | Tombstone a fact disappears from `cmk search` immediately, no manual reindex (audit trail preserved) |
78
- | `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 |
79
- | `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`) |
80
- | `cmk persona generate` | Run cross-project persona synthesis on demand (instead of waiting for the weekly pass) |
81
- | `cmk persona export <file>` / `import <file>` | Carry your cross-project persona (the user tier) to another of **your** machines export to one portable bundle, import on the other (overwrites with backup + rollback). The persona stays private (never committed to a project) |
82
- | `cmk import-anthropic-memory [--dry-run] [--yes]` | Merge bullets from Anthropic's native auto-memory into MEMORY.md |
83
- | `cmk import-claude-md [file] [--dry-run] [--yes]` | Onboard from the rules you already own — parse an existing `CLAUDE.md` / `.cursorrules` / `AGENTS.md` into typed facts through the safe write path (Poison_Guard + sanitization + dedup) |
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.
84
173
 
85
174
  ## Requirements
86
175
 
87
176
  - Node.js ≥ 20
88
- - Claude Code (for the hook-driven auto-memory loop)
177
+ - Claude Code (for the hook-driven auto-memory loop) — or [Kiro](https://kiro.dev)
89
178
  - Optional: `cmk install --with-semantic` for semantic/hybrid recall (installs the local `@huggingface/transformers` embedder, ~260 MB once — no API, no Python)
90
179
 
91
- ## Three-tier model
92
-
93
- | Tier | Location | Scope |
94
- | --- | --- | --- |
95
- | **P** (project) | `<repo>/context/` | committed to git, travels with `clone` |
96
- | **L** (local) | `<repo>/context.local/` | gitignored, per-machine |
97
- | **U** (user) | `~/.claude-memory-kit/` | cross-project per-user |
98
-
99
180
  ## Documentation
100
181
 
101
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 });
@@ -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.4.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": {
@@ -13,7 +13,8 @@
13
13
  "cmk-observe-edit": "./bin/cmk-observe-edit.mjs",
14
14
  "cmk-capture-turn": "./bin/cmk-capture-turn.mjs",
15
15
  "cmk-compress-session": "./bin/cmk-compress-session.mjs",
16
- "cmk-guard-memory": "./bin/cmk-guard-memory.mjs"
16
+ "cmk-guard-memory": "./bin/cmk-guard-memory.mjs",
17
+ "cmk-approve-permission": "./bin/cmk-approve-permission.mjs"
17
18
  },
18
19
  "files": [
19
20
  "bin/",
@@ -0,0 +1,92 @@
1
+ // PermissionRequest auto-approve logic — the prompt-free fix (Task 172).
2
+ //
3
+ // Background (the v0.4.1 cut-gate, 2026-06-27). Claude Code 2.1.x tightened
4
+ // permission matching so that neither the kit's `permissions.allow` MCP rules
5
+ // (`mcp__cmk__*` + the specific names, Task 171) NOR a skill's `allowed-tools`
6
+ // grant reliably suppress the per-tool MCP approval prompt or the "Use skill?"
7
+ // prompt — see anthropics/claude-code#17499 (allowed-tools for MCP tools is
8
+ // undocumented/unreliable) and #18837 → #14956 (allowed-tools not enforced).
9
+ // The popup is a CLAUDE CODE change, not a kit regression: the skill + allow-list
10
+ // config has been byte-stable since Task 108/117.
11
+ //
12
+ // The DOCUMENTED, working mechanism is a `PermissionRequest` hook
13
+ // (code.claude.com/docs/en/hooks-guide#auto-approve-specific-permission-prompts):
14
+ // it fires when CC is about to show a permission dialog, and a hook that writes
15
+ // `{"behavior":"allow"}` answers it on the user's behalf — proven live (v041l):
16
+ // the dialog flashes then auto-dismisses, the tool runs, no click required.
17
+ //
18
+ // This module decides — given the hook payload — whether to auto-approve. It
19
+ // approves ONLY the kit's OWN surfaces (its MCP tools + its scaffolded skills),
20
+ // never anything else. Two-layer safety: the wired matcher is already narrow
21
+ // (`mcp__cmk__.*` / `Skill`), and this self-check is the second layer, so even a
22
+ // loose matcher can't make the hook approve a non-kit tool. Returns `null` for
23
+ // anything not kit-owned → the hook stays silent → CC's normal permission flow
24
+ // runs unchanged.
25
+
26
+ // The kit's own scaffolded skills (template/.claude/skills/<name>). Keep in sync
27
+ // with the Skill(...) entries in settings-hooks.mjs KIT_ALLOW.
28
+ const KIT_SKILLS = Object.freeze(['memory-write', 'memory-search']);
29
+
30
+ // The allow decision shape CC expects on stdout for a PermissionRequest hook.
31
+ export const ALLOW_DECISION = Object.freeze({
32
+ hookSpecificOutput: {
33
+ hookEventName: 'PermissionRequest',
34
+ decision: { behavior: 'allow' },
35
+ },
36
+ });
37
+
38
+ /**
39
+ * True iff `toolName` is one of the kit's own MCP tools (any `mcp__cmk__<tool>`).
40
+ */
41
+ function isKitMcpTool(toolName) {
42
+ return typeof toolName === 'string' && toolName.startsWith('mcp__cmk__');
43
+ }
44
+
45
+ /**
46
+ * True iff this is an invocation of one of the kit's own scaffolded skills.
47
+ * The Skill tool surfaces the skill name either as the tool name's suffix
48
+ * (`Skill(memory-write)`) or in `tool_input.name`/`tool_input.skill` — accept
49
+ * either shape so a CC payload-format change doesn't silently stop matching.
50
+ *
51
+ * Security boundary: the `tool_input.name`/`skill` fallback is consulted ONLY
52
+ * when `tool_name` identifies the Skill tool (`Skill` or `Skill(...)`). Without
53
+ * that gate, a non-Skill request whose `tool_input` merely happened to carry
54
+ * `{name:"memory-write"}` (e.g. a `Bash` call) would match — defeating the
55
+ * second layer of defence-in-depth (the matcher is the first). We never read
56
+ * `tool_input` for a tool that isn't the Skill tool.
57
+ */
58
+ function isKitSkill(toolName, toolInput) {
59
+ if (typeof toolName !== 'string') return false;
60
+ // The documented tool-name form for a skill invocation: `Skill(<name>)`.
61
+ for (const skill of KIT_SKILLS) {
62
+ if (toolName === `Skill(${skill})`) return true;
63
+ }
64
+ // Only trust the tool_input name shape for an actual Skill-tool request.
65
+ // (A bare `tool_name === "<skill>"` is NOT matched — it isn't a documented
66
+ // CC shape and would risk approving any unrelated tool that happened to share
67
+ // the name.)
68
+ if (toolName === 'Skill' || toolName.startsWith('Skill(')) {
69
+ const named = toolInput && (toolInput.name ?? toolInput.skill ?? toolInput.skillName);
70
+ if (typeof named === 'string' && KIT_SKILLS.includes(named)) return true;
71
+ }
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Decide whether to auto-approve a PermissionRequest for a kit-owned surface.
77
+ *
78
+ * @param {object} payload - the hook payload from stdin
79
+ * ({ tool_name, tool_input, ... }).
80
+ * @returns {object|null} the ALLOW_DECISION object to print on stdout, or null
81
+ * when the request is NOT for a kit surface (the hook emits nothing → CC's
82
+ * normal permission flow proceeds).
83
+ */
84
+ export function evaluatePermissionRequest(payload) {
85
+ if (!payload || typeof payload !== 'object') return null;
86
+ const toolName = payload.tool_name;
87
+ const toolInput = payload.tool_input;
88
+ if (isKitMcpTool(toolName) || isKitSkill(toolName, toolInput)) {
89
+ return ALLOW_DECISION;
90
+ }
91
+ return null;
92
+ }
@@ -858,18 +858,25 @@ export async function runAutoExtract({
858
858
  // ceiling is free. Live-test finding (2026-06-01, live-test-4 baseline).
859
859
  timeoutMs: 90_000,
860
860
  });
861
- // Touch the cooldown marker IMMEDIATELY after the Haiku call
862
- // resolves — this is the "we spent the budget" signal that
863
- // compress-session.mjs reads to skip its own Haiku call within
864
- // 120s of ours. Touching on success only (not in the catch below)
865
- // would mean a failing Haiku in the auto-extract path doesn't
866
- // block compress-session which would then re-spend the budget
867
- // on the failure. The catch path below also touches.
861
+ // Touch the cooldown marker after a SUCCESSFUL Haiku call — the
862
+ // "we spent the budget" signal compress-session.mjs reads to skip its
863
+ // own Haiku call within 120s of ours.
864
+ //
865
+ // **Original behavior (pre-Task-167, preserved as decision-trail):** the
866
+ // catch block below ALSO touched the cooldown ("spent the budget,
867
+ // succeeded OR failed"), so a failing Haiku here blocked compress-session
868
+ // for 120s — on the theory that a failure shouldn't let the next caller
869
+ // re-spend the budget on the same broken call.
870
+ // **Task 167.F change (D-207, Q5):** the cooldown now fires on SUCCESS
871
+ // ONLY. A FAILED call did NOT successfully spend the budget — blocking the
872
+ // next NEEDED compress for 120s after a *transient* failure is the wrong
873
+ // gate (it was the SECONDARY cause of the 410 KB now.md bloat: after the
874
+ // dead cron, failed-call cooldowns kept the roll skipped). Correctness >
875
+ // cost — a transient failure must be free to retry.
868
876
  touchCooldownMarker({ projectRoot, now: ts });
869
877
  } catch (err) {
870
- // Spent the Haiku budget (succeeded OR failed); touch the
871
- // cooldown so compress-session skips within 120s.
872
- touchCooldownMarker({ projectRoot, now: ts });
878
+ // Task 167.F: do NOT touch the cooldown on failure (see the success-path
879
+ // comment above) a failed call must not block the next compress's retry.
873
880
  // Route on the error TYPE — distinguishes "took too long"
874
881
  // (HAIKU_TIMEOUT) from "subprocess exited non-zero"
875
882
  // (HAIKU_FAILED). Using `instanceof HaikuTimeoutError`
@@ -336,12 +336,16 @@ export async function autoPersona(opts = {}) {
336
336
  // ceiling, so it passes a generous value — the explicit command can wait.
337
337
  timeoutMs,
338
338
  });
339
- // Spent a Haiku call — refresh the shared cooldown marker so the next
340
- // gated caller backs off. (touch even on cooldownMs:0 cycles: the call
339
+ // Spent a Haiku call SUCCESSFULLY — refresh the shared cooldown marker so the
340
+ // next gated caller backs off. (touch even on cooldownMs:0 cycles: the call
341
341
  // happened, so the marker should reflect it for any LATER gated caller.)
342
342
  touchCooldownMarker({ projectRoot, now: ts });
343
343
  } catch (err) {
344
- touchCooldownMarker({ projectRoot, now: ts });
344
+ // Task 167.F (D-207, Q5): do NOT touch the cooldown on FAILURE — a failed
345
+ // Haiku call did not successfully spend the budget, and blocking the next
346
+ // needed compress for 120s after a transient failure is the wrong gate
347
+ // (correctness > cost; a transient failure must be free to retry). Original
348
+ // pre-167 behavior touched here too; preserved as decision-trail.
345
349
  return errorResult({
346
350
  category: ERROR_CATEGORIES.COMPRESS_FAILED,
347
351
  errors: [err?.message ?? String(err)],