memhook 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to memhook are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0](https://github.com/utilia-ai-wox/memhook/compare/v0.2.2...v0.3.0) (2026-06-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * add init/uninstall setup + tail live monitor ([#29](https://github.com/utilia-ai-wox/memhook/issues/29)) ([e895c9a](https://github.com/utilia-ai-wox/memhook/commit/e895c9a44fb46a141a20f1716999be3a0208bb89))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * harden hook path against file-system races and stdin errors ([#30](https://github.com/utilia-ai-wox/memhook/issues/30)) ([8a0f10e](https://github.com/utilia-ai-wox/memhook/commit/8a0f10e1d7a7184b7cc5fdcb4eb20ae4be516b0d))
19
+
20
+
21
+ ### Documentation
22
+
23
+ * refresh all docs for v0.2.2 and revamp the README ([#27](https://github.com/utilia-ai-wox/memhook/issues/27)) ([ad1106a](https://github.com/utilia-ai-wox/memhook/commit/ad1106a3a0cea47ce75201a781fdf5eda80324b2))
24
+
8
25
  ## [0.2.2](https://github.com/utilia-ai-wox/memhook/compare/v0.2.1...v0.2.2) (2026-06-02)
9
26
 
10
27
 
@@ -45,75 +62,9 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
45
62
 
46
63
  * graduate release line to clean 0.2.0 ([f0a07e5](https://github.com/utilia-ai-wox/memhook/commit/f0a07e51fe5792df4efd7af9d59452d97603b675))
47
64
 
48
- ## [Unreleased]
49
-
50
- ### Added
51
-
52
- - **OpenAI provider** (`MEMHOOK_PROVIDER=openai`) Chat Completions API,
53
- `Authorization: Bearer`, catalog as the leading system message so OpenAI's
54
- automatic prompt caching can engage. Default model `gpt-4o-mini`.
55
- - **Ollama local provider** (`MEMHOOK_PROVIDER=ollama`) — native `/api/chat`
56
- endpoint, no API key, `stream:false` + `format:"json"`. Default model
57
- `llama3.1`, with a 30s default timeout to absorb cold model loads.
58
- - **YAML config file** (`config.yaml`) — optional, opt-in, read from
59
- `$MEMHOOK_CONFIG` or `~/.config/memhook/config.yaml`. Precedence is
60
- env var > YAML > default. A missing or malformed file is ignored
61
- (fail-soft to defaults). See `config.example.yaml`.
62
- - `src/providers/factory.ts` — `createProvider()` selects the adapter from
63
- `config.provider.type` with compile-time exhaustiveness.
64
- - `src/providers/http.ts` — single shared `postJsonWithRetry` transport
65
- (timeout + single retry) used by all providers.
66
- - `MEMHOOK_PROVIDER` and `MEMHOOK_CONFIG` env vars; per-provider defaults for
67
- model / API-key env var / timeout.
68
- - Hardening pre-publish (CI, CHANGELOG, CONTRIBUTING, `.env.example`)
69
-
70
- ### Changed
71
-
72
- - The provider interface is now provider-agnostic: Anthropic-specific
73
- `betaHeaders` and `cacheControlTtl` moved off the shared `SelectionRequest`
74
- into `AnthropicProviderOptions`. `ProviderConfig.apiKey` is now optional
75
- (local providers need none).
76
- - Cache key now includes the provider identity (`type:model`) so switching
77
- provider or model never serves a selection made by a different model.
78
- - The two hardcoded version strings (`config.ts`, `bin/memhook.ts`) are
79
- centralised in `src/version.ts`.
80
-
81
- ### Note
82
-
83
- - Adding `openai` / `ollama` introduces opt-in outbound calls to
84
- `api.openai.com` / `localhost:11434`. The default remains Anthropic-only;
85
- `api.anthropic.com` is still the sole endpoint for an unconfigured user. No
86
- telemetry, no phone-home.
87
- - First runtime dependency: `yaml` (zero sub-dependencies).
88
-
89
- ## [0.1.0-preview.0] — 2026-05-28
90
-
91
- Initial public preview.
92
-
93
- ### Added
94
-
95
- - `src/router.ts` — UserPromptSubmit hook entry point with cap-A1 projection
96
- fix (skip a file pre-injection if its content would push the cumulated
97
- injection past `maxAdditionalChars`, while always allowing at least one).
98
- - `src/catalog.ts` — catalog builder with Q4 title-only reduction for
99
- non-CWD zones (~50% size cut on a typical 3-repo layout).
100
- - `src/cache.ts` — local LRU cache keyed on
101
- `sha256(prompt + catalog_mtime + cwd + script_version)`. Stored as
102
- per-key JSON files. 60-min TTL by default, 7-day eviction floor.
103
- - `src/preFilter.ts` — trivial-prompt pre-filter loaded from
104
- `~/.config/memhook/trivial-words.txt` with a sensible default list.
105
- - `src/providers/anthropic.ts` — provider implementation for Anthropic
106
- Messages API. Uses `ephemeral` `1h` cache control on the system prompt
107
- (GA in 2026, no beta header) so the catalog sits in cache (10× cheaper
108
- writes amortised across the hour).
109
- - `src/config.ts` — env-driven config loader. No YAML in v0.1; deferred to
110
- v0.2.
111
- - `bin/memhook.ts` — CLI with `run`, `build-catalog`, `version`, `help`.
112
- - JSONL observability log at `~/.claude/logs/memhook.log` including
113
- `additional_size_chars` + `additional_size_tokens_est` so users can audit
114
- the actual size of the injected `additionalContext` over time.
115
- - 18 unit tests covering the router pipeline, pre-filter normalisation, and
116
- cache key derivation / TTL / eviction.
117
-
118
- [Unreleased]: https://github.com/utilia-ai-wox/memhook/compare/v0.1.0-preview.0...HEAD
119
- [0.1.0-preview.0]: https://github.com/utilia-ai-wox/memhook/releases/tag/v0.1.0-preview.0
65
+ ## [0.1.0-preview.0](https://github.com/utilia-ai-wox/memhook/releases/tag/v0.1.0-preview.0) (2026-05-28)
66
+
67
+ Initial public preview: Anthropic Haiku provider, fail-soft pipeline, cap-A1
68
+ projection fix, JSONL observability log, catalog builder, local LRU cache,
69
+ trivial-prompt pre-filter, and the `memhook run | build-catalog | version`
70
+ CLI.
package/README.md CHANGED
@@ -1,91 +1,128 @@
1
+ <div align="center">
2
+
1
3
  # memhook
2
4
 
3
- > Semantic memory router for Claude Code picks the relevant feedbacks &
4
- > rules for each prompt via Haiku, injects them as `additionalContext`.
5
+ **Stop loading every memory file on every prompt. memhook routes only the relevant ones.**
5
6
 
6
- **Status**: `v0.1.0-preview` — extracted from a private hook used daily across
7
- 3 large repos. API surface and naming may shift before `v1.0.0`.
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/memhook"><img src="https://img.shields.io/npm/v/memhook?color=cb3837&logo=npm" alt="npm version"></a>
9
+ <a href="https://www.npmjs.com/package/memhook"><img src="https://img.shields.io/npm/dm/memhook?color=cb3837" alt="npm downloads"></a>
10
+ <a href="https://github.com/utilia-ai-wox/memhook/actions/workflows/ci.yml"><img src="https://github.com/utilia-ai-wox/memhook/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
+ <a href="https://github.com/utilia-ai-wox/memhook/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/memhook?color=blue" alt="License: MIT"></a>
12
+ <img src="https://img.shields.io/node/v/memhook" alt="Node version">
13
+ <a href="https://github.com/utilia-ai-wox/memhook/stargazers"><img src="https://img.shields.io/github/stars/utilia-ai-wox/memhook?style=social" alt="GitHub stars"></a>
14
+ </p>
8
15
 
9
- ## Why
16
+ A semantic memory router for [Claude Code](https://claude.com/claude-code) — a
17
+ `UserPromptSubmit` hook that picks the relevant `feedback_*.md` & `rule_*.md`
18
+ files for each prompt and injects them as `additionalContext`.
10
19
 
11
- Claude Code's `~/.claude/` directory accumulates a growing set of
12
- `feedback_*.md` (behavioural corrections) and `rule_*.md` (project doctrine)
13
- files. Loading all of them on every prompt is wasteful (10–14k tokens of
14
- catalog overhead, most of it irrelevant to the current question).
20
+ </div>
15
21
 
16
- memhook uses **Haiku 4.5** as a cheap router: each user prompt is matched
17
- against a one-line catalog of all available memory files, and only the 0–5
18
- most relevant ones are read and injected into `additionalContext`. The rest
19
- sit on disk, invisible to Claude until they matter.
22
+ <!-- TODO(demo): record an asciinema of `tail -f ~/.claude/logs/memhook.log`
23
+ while prompting Claude Code it shows the router picking files live.
24
+ Embed: [![asciicast](https://asciinema.org/a/XXXXX.svg)](https://asciinema.org/a/XXXXX) -->
20
25
 
21
- ## How it works
26
+ ## Features
22
27
 
23
- ```
24
- UserPromptSubmit hook
25
-
26
-
27
- ┌────────────────────────────────────────┐
28
- │ 1. Pre-filter trivial prompts │ ── "ok" / "merci" skip LLM
29
- 2. Check local LRU cache ── identical prompt < 60min hit
30
- 3. Call Haiku with catalog as system │ ── ephemeral 1h cache control
31
- │ 4. Parse JSON array of basenames │ ── ["feedback_X.md", "rule_Y.md"]
32
- │ 5. Read files, cap by token budget │ ── max 9.5k chars or 5 files
33
- │ 6. Emit additionalContext │
34
- └────────────────────────────────────────┘
35
- ```
28
+ - 🎯 **Relevant-only injection** — a cheap model picks the 0–5 memory files that matter for _this_ prompt.
29
+ - 💸 **Token-frugal** — skips the 10–14k-token catalog dump; injects ~2k tokens of signal.
30
+ - 🛡️ **Fail-soft** — never blocks Claude Code; every error path falls back to empty context.
31
+ - 🔌 **Multi-provider** — Anthropic (default), OpenAI, or local Ollama. Your key, your endpoint.
32
+ - 🤫 **Zero telemetry** — the only outbound call is the LLM endpoint _you_ chose.
33
+ - 🪶 **One dependency** `yaml`, with zero sub-deps.
34
+ - **Cached & pre-filtered** — an LRU cache + a trivial-prompt skip keep latency near zero.
35
+ - 🧰 **One-command setup** `memhook init` wires the hooks (with backup); `memhook tail` shows routing live.
36
36
 
37
- ## Install
37
+ ## 🤔 Why
38
38
 
39
- ```bash
40
- npm install -g memhook # not yet published — see "From source" below
41
- ```
39
+ Claude Code's `~/.claude/` directory accumulates a growing set of
40
+ `feedback_*.md` (behavioural corrections) and `rule_*.md` (project doctrine)
41
+ files. Loading all of them on every prompt is wasteful — most of it is
42
+ irrelevant to the question at hand.
43
+
44
+ memhook uses a cheap router model (**Haiku 4.5** by default) to match each
45
+ prompt against a one-line catalog of all your memory files, and injects only
46
+ the most relevant ones. The rest sit on disk, invisible until they matter.
42
47
 
43
- ### From source (preview)
48
+ | Approach | Tokens / prompt | Relevance |
49
+ | --------------------- | --------------- | ----------------- |
50
+ | Load all memory files | 10–14k | mostly irrelevant |
51
+ | **memhook** | ~2k | only what matches |
52
+
53
+ ## 🚀 Quick start
44
54
 
45
55
  ```bash
46
- git clone https://github.com/utilia-ai-wox/memhook.git
47
- cd memhook
48
- bun install
49
- bun run build
50
- npm link
56
+ npm install -g memhook
57
+ export ANTHROPIC_API_KEY=sk-ant-… # or see Providers for OpenAI / Ollama
58
+ memhook init
51
59
  ```
52
60
 
53
- ## Setup
61
+ `memhook init` detects your Claude Code config, wires the two hooks into
62
+ `~/.claude/settings.json` (backing it up first, never clobbering existing
63
+ hooks), and builds the initial catalog. It is idempotent and supports
64
+ `--dry-run`. Restart Claude Code and you're done — then watch it work live
65
+ with [`memhook tail`](#-observability).
54
66
 
55
- 1. **Set your API key**
56
-
57
- ```bash
58
- export ANTHROPIC_API_KEY=sk-ant-…
59
- ```
67
+ <details>
68
+ <summary>Manual setup (what <code>init</code> automates)</summary>
60
69
 
61
- 2. **Build the initial catalog**
70
+ 1. **Build the initial catalog**
62
71
 
63
72
  ```bash
64
73
  memhook build-catalog
65
74
  # → ~/.claude/cache/memory-catalog.txt
66
75
  ```
67
76
 
68
- 3. **Wire the hooks** in `~/.claude/settings.json`:
77
+ 2. **Wire the hooks** in `~/.claude/settings.json`:
69
78
 
70
79
  ```json
71
80
  {
72
81
  "hooks": {
73
82
  "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "memhook run" }] }],
74
- "SessionStart": [
75
- {
76
- "hooks": [{ "type": "command", "command": "memhook build-catalog" }]
77
- }
78
- ]
83
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "memhook build-catalog" }] }]
79
84
  }
80
85
  }
81
86
  ```
82
87
 
83
- ## Configuration
88
+ Remove it all later with `memhook uninstall` (also backs up first).
89
+
90
+ </details>
84
91
 
85
- Every knob is an env var, and (since `v0.2`) optionally a YAML file.
86
- Precedence per key is **env var > YAML file > built-in default**, so an
87
- env-var-only setup behaves exactly as before. Sensible defaults work for most
88
- users.
92
+ <details>
93
+ <summary>From source (for contributors)</summary>
94
+
95
+ ```bash
96
+ git clone https://github.com/utilia-ai-wox/memhook.git
97
+ cd memhook
98
+ bun install
99
+ bun run build
100
+ npm link
101
+ ```
102
+
103
+ </details>
104
+
105
+ ## 🔍 How it works
106
+
107
+ ```
108
+ UserPromptSubmit hook
109
+
110
+
111
+ ┌────────────────────────────────────────┐
112
+ │ 1. Pre-filter trivial prompts │ ── "ok" / "merci" → skip LLM
113
+ │ 2. Check local LRU cache │ ── identical prompt < 60min → hit
114
+ │ 3. Call the router with catalog │ ── ephemeral 1h cache control
115
+ │ 4. Parse JSON array of basenames │ ── ["feedback_X.md", "rule_Y.md"]
116
+ │ 5. Read files, cap by token budget │ ── max 9.5k chars or 5 files
117
+ │ 6. Emit additionalContext │
118
+ └────────────────────────────────────────┘
119
+ ```
120
+
121
+ ## ⚙️ Configuration
122
+
123
+ Every knob is an env var, and optionally a YAML file. Precedence per key is
124
+ **env var > YAML file > built-in default**, so an env-var-only setup behaves
125
+ exactly as before. Sensible defaults work for most users.
89
126
 
90
127
  | Variable | Default | Purpose |
91
128
  | -------------------------------- | ------------------------------- | -------------------------------------- |
@@ -119,7 +156,7 @@ selection:
119
156
  maxFiles: 5
120
157
  ```
121
158
 
122
- ## Providers
159
+ ## 🔌 Providers
123
160
 
124
161
  The default provider is **Anthropic** — with no `MEMHOOK_PROVIDER` set, the
125
162
  only outbound call memhook ever makes is to `api.anthropic.com`, using your own
@@ -141,7 +178,7 @@ LLM endpoint _you_ choose to route through.
141
178
  the native `/api/chat` endpoint with `stream:false`; the timeout defaults to
142
179
  30s to absorb cold model loads.
143
180
 
144
- ## Observability
181
+ ## 📊 Observability
145
182
 
146
183
  Every invocation appends one JSON line to `~/.claude/logs/memhook.log`:
147
184
 
@@ -157,18 +194,34 @@ Every invocation appends one JSON line to `~/.claude/logs/memhook.log`:
157
194
  "cache_read": 13398,
158
195
  "additional_size_chars": 20225,
159
196
  "additional_size_tokens_est": 5056,
160
- "status": "ok"
197
+ "status": "ok",
198
+ "model": "claude-haiku-4-5"
161
199
  }
162
200
  ```
163
201
 
164
- Useful one-liner to inspect the last 7 days:
202
+ ### Live view `memhook tail`
203
+
204
+ Watch routing decisions as they happen, in colour:
205
+
206
+ ```bash
207
+ memhook tail # follow live (Ctrl-C to quit)
208
+ memhook tail --no-follow # print recent log + summary, then exit
209
+ memhook tail --status ok,cache_hit # filter by status
210
+ memhook tail -n 50 # show more history first
211
+ ```
212
+
213
+ Each row shows the time, status, prompt preview, latency, and model, plus the
214
+ memories that were injected; a summary line reports the cache-hit rate and
215
+ p50/p95 latency. Colour degrades to plain text when piped or under `NO_COLOR`.
216
+ `tail` only reads the log, so it can never affect the hook. For raw analysis,
217
+ the log is plain JSONL — e.g. the last 7 days by status:
165
218
 
166
219
  ```bash
167
220
  jq -c 'select((.ts | fromdateiso8601) > (now - 7*86400)) | .status' \
168
221
  ~/.claude/logs/memhook.log | sort | uniq -c
169
222
  ```
170
223
 
171
- ## Status values
224
+ ### Status values
172
225
 
173
226
  | `status` | Meaning |
174
227
  | ---------------------- | ----------------------------------------------------- |
@@ -184,20 +237,29 @@ jq -c 'select((.ts | fromdateiso8601) > (now - 7*86400)) | .status' \
184
237
  | `api_no_content` | API returned 200 but no text |
185
238
  | `parse_invalid` | Response wasn't a valid JSON array |
186
239
 
187
- ## Fail-soft
240
+ ## 🛡️ Fail-soft
188
241
 
189
242
  memhook never blocks Claude Code. On any error — missing key, network
190
243
  timeout, malformed JSON, broken filesystem — it emits an empty
191
- `additionalContext` and logs the status. Your prompt always reaches the
192
- model, just without injected memories for that turn.
244
+ `additionalContext` and logs the status. **Your prompt always reaches the
245
+ model**, just without injected memories for that turn.
193
246
 
194
- ## Roadmap
247
+ ## 🗺️ Roadmap
195
248
 
196
- - `v0.2` ✅ — YAML config file, OpenAI provider, Ollama local provider
197
- - `v0.3` — TUI live monitor (`memhook tail`)
249
+ - `v0.2` ✅ — YAML config file, OpenAI provider, Ollama local provider (published on npm)
250
+ - `v0.3` — `memhook init` / `memhook uninstall` setup wizard + zero-dep live monitor (`memhook tail`)
198
251
  - `v0.4` — Companion skills (`/wrap`, `/curate`, `/relay`)
199
- - `v0.5` — Auto-bootstrap (`memhook init` detects empty memory dirs)
200
- - `v1.0` — Cross-platform validated, published to npm
252
+ - `v1.0` — API frozen, cross-platform validated, listed on awesome-lists
253
+
254
+ ## 🤝 Contributing
255
+
256
+ Contributions welcome — please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
257
+ The hook contract (fail-soft, no telemetry, strict TypeScript) is
258
+ non-negotiable; the [`failsoft-auditor`](.claude/agents/failsoft-auditor.md)
259
+ agent guards it on every PR.
260
+
261
+ > [!TIP]
262
+ > ⭐ If memhook saves you tokens, **star the repo** — it helps other Claude Code users find it.
201
263
 
202
264
  ## License
203
265
 
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * memhook CLI — three commands shipped in v0.1 preview:
3
+ * memhook CLI.
4
4
  *
5
5
  * memhook run Read stdin (Claude Code hook JSON), write hook output
6
6
  * memhook build-catalog Rebuild ~/.claude/cache/memory-catalog.txt
7
+ * memhook init Wire memhook into ~/.claude/settings.json (interactive)
8
+ * memhook uninstall Remove memhook's hooks from ~/.claude/settings.json
9
+ * memhook tail Pretty live view of the JSONL routing log
7
10
  * memhook version Print package version
8
11
  *
9
- * Designed to be wired into ~/.claude/settings.json hooks:
10
- * "UserPromptSubmit": [{ "hooks": [{ "type": "command",
11
- * "command": "memhook run" }] }]
12
- * "SessionStart": [{ "hooks": [{ "type": "command",
13
- * "command": "memhook build-catalog" }] }]
12
+ * Only `run` obeys the fail-soft hook contract (never throws, never exits
13
+ * non-zero). The interactive commands (`init`/`uninstall`/`tail`) may exit
14
+ * non-zero on user error and are free to use the TTY — docs/SPECIFICATION.md §9.
15
+ *
16
+ * Wired into ~/.claude/settings.json hooks (see `memhook init`):
17
+ * "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "memhook run" }] }]
18
+ * "SessionStart": [{ "hooks": [{ "type": "command", "command": "memhook build-catalog" }] }]
14
19
  */
15
20
  export {};
16
21
  //# sourceMappingURL=memhook.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memhook.d.ts","sourceRoot":"","sources":["../../bin/memhook.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"}
1
+ {"version":3,"file":"memhook.d.ts","sourceRoot":"","sources":["../../bin/memhook.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -1,23 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * memhook CLI — three commands shipped in v0.1 preview:
3
+ * memhook CLI.
4
4
  *
5
5
  * memhook run Read stdin (Claude Code hook JSON), write hook output
6
6
  * memhook build-catalog Rebuild ~/.claude/cache/memory-catalog.txt
7
+ * memhook init Wire memhook into ~/.claude/settings.json (interactive)
8
+ * memhook uninstall Remove memhook's hooks from ~/.claude/settings.json
9
+ * memhook tail Pretty live view of the JSONL routing log
7
10
  * memhook version Print package version
8
11
  *
9
- * Designed to be wired into ~/.claude/settings.json hooks:
10
- * "UserPromptSubmit": [{ "hooks": [{ "type": "command",
11
- * "command": "memhook run" }] }]
12
- * "SessionStart": [{ "hooks": [{ "type": "command",
13
- * "command": "memhook build-catalog" }] }]
12
+ * Only `run` obeys the fail-soft hook contract (never throws, never exits
13
+ * non-zero). The interactive commands (`init`/`uninstall`/`tail`) may exit
14
+ * non-zero on user error and are free to use the TTY — docs/SPECIFICATION.md §9.
15
+ *
16
+ * Wired into ~/.claude/settings.json hooks (see `memhook init`):
17
+ * "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "memhook run" }] }]
18
+ * "SessionStart": [{ "hooks": [{ "type": "command", "command": "memhook build-catalog" }] }]
14
19
  */
15
20
  import { route } from "../src/router.js";
16
21
  import { buildCatalog } from "../src/catalog.js";
17
22
  import { loadConfig } from "../src/config.js";
23
+ import { runInit, runUninstall } from "../src/init.js";
24
+ import { runTail } from "../src/tail.js";
18
25
  import { MEMHOOK_VERSION as VERSION } from "../src/version.js";
26
+ const PROVIDERS = ["anthropic", "openai", "ollama"];
19
27
  async function main() {
20
28
  const cmd = process.argv[2] ?? "help";
29
+ const args = process.argv.slice(3);
21
30
  switch (cmd) {
22
31
  case "run":
23
32
  await cmdRun();
@@ -25,6 +34,15 @@ async function main() {
25
34
  case "build-catalog":
26
35
  cmdBuildCatalog();
27
36
  break;
37
+ case "init":
38
+ process.exitCode = await cmdInit(args);
39
+ break;
40
+ case "uninstall":
41
+ process.exitCode = await cmdUninstall(args);
42
+ break;
43
+ case "tail":
44
+ process.exitCode = await cmdTail(args);
45
+ break;
28
46
  case "version":
29
47
  case "--version":
30
48
  case "-v":
@@ -42,19 +60,20 @@ async function main() {
42
60
  }
43
61
  }
44
62
  async function cmdRun() {
45
- const stdin = await readStdin();
46
- let output;
63
+ // Fail-soft: read stdin AND route inside one try, so ANY error — a stdin
64
+ // read error (stdin 'error' event) just as much as a routing error — falls
65
+ // back to empty additionalContext and exit 0, never a non-zero exit that
66
+ // would block the user prompt.
67
+ let output = {
68
+ hookSpecificOutput: {
69
+ hookEventName: "UserPromptSubmit",
70
+ additionalContext: "",
71
+ },
72
+ };
47
73
  try {
48
- output = await route(stdin);
74
+ output = await route(await readStdin());
49
75
  }
50
76
  catch (err) {
51
- // Fail-soft: emit empty additionalContext, never block the user prompt
52
- output = {
53
- hookSpecificOutput: {
54
- hookEventName: "UserPromptSubmit",
55
- additionalContext: "",
56
- },
57
- };
58
77
  if (process.env["MEMHOOK_DEBUG"] === "true") {
59
78
  process.stderr.write(`memhook run error: ${String(err)}\n`);
60
79
  }
@@ -69,6 +88,100 @@ function cmdBuildCatalog() {
69
88
  });
70
89
  process.stderr.write(`[memhook build-catalog] ${config.catalog.path} — ${result.lines}L, ${result.bytes}B\n`);
71
90
  }
91
+ async function cmdInit(args) {
92
+ const { flags } = parseArgs(args, BOOL_INIT);
93
+ let provider;
94
+ if (typeof flags["provider"] === "string") {
95
+ if (!PROVIDERS.includes(flags["provider"])) {
96
+ process.stderr.write(`memhook init: unknown provider "${flags["provider"]}"\n`);
97
+ return 1;
98
+ }
99
+ provider = flags["provider"];
100
+ }
101
+ return runInit({
102
+ yes: flags["yes"] === true,
103
+ dryRun: flags["dry-run"] === true,
104
+ provider,
105
+ apiKeyEnv: strFlag(flags["api-key-env"]),
106
+ model: strFlag(flags["model"]),
107
+ bin: strFlag(flags["bin"]) ?? "memhook",
108
+ settingsPath: strFlag(flags["settings"]),
109
+ noCatalog: flags["no-catalog"] === true,
110
+ });
111
+ }
112
+ async function cmdUninstall(args) {
113
+ const { flags } = parseArgs(args, BOOL_UNINSTALL);
114
+ return runUninstall({
115
+ yes: flags["yes"] === true,
116
+ dryRun: flags["dry-run"] === true,
117
+ settingsPath: strFlag(flags["settings"]),
118
+ purge: flags["purge"] === true,
119
+ });
120
+ }
121
+ async function cmdTail(args) {
122
+ const { flags } = parseArgs(args, BOOL_TAIL);
123
+ const linesRaw = strFlag(flags["lines"]);
124
+ const lines = linesRaw !== undefined && /^\d+$/.test(linesRaw) ? Number(linesRaw) : 10;
125
+ const statusRaw = strFlag(flags["status"]);
126
+ const status = statusRaw
127
+ ? statusRaw
128
+ .split(",")
129
+ .map((s) => s.trim())
130
+ .filter(Boolean)
131
+ : undefined;
132
+ return runTail({
133
+ file: strFlag(flags["file"]),
134
+ lines,
135
+ noFollow: flags["no-follow"] === true,
136
+ status,
137
+ });
138
+ }
139
+ // ── tiny flag parser ─────────────────────────────────────────────────────────
140
+ const BOOL_INIT = new Set(["yes", "dry-run", "no-catalog"]);
141
+ const BOOL_UNINSTALL = new Set(["yes", "dry-run", "purge"]);
142
+ const BOOL_TAIL = new Set(["no-follow"]);
143
+ const SHORT = { "-y": "--yes", "-n": "--lines" };
144
+ function strFlag(v) {
145
+ return typeof v === "string" ? v : undefined;
146
+ }
147
+ /**
148
+ * Parse `--key value`, `--key=value`, and boolean flags (those listed in
149
+ * `bools`, plus any `--key` not followed by a value). Unknown long flags that
150
+ * take a value consume the next non-dash token.
151
+ */
152
+ function parseArgs(args, bools) {
153
+ const flags = {};
154
+ const positionals = [];
155
+ for (let i = 0; i < args.length; i++) {
156
+ const raw = args[i];
157
+ if (raw === undefined)
158
+ continue;
159
+ const a = SHORT[raw] ?? raw;
160
+ if (!a.startsWith("--")) {
161
+ positionals.push(a);
162
+ continue;
163
+ }
164
+ const key = a.slice(2);
165
+ const eq = key.indexOf("=");
166
+ if (eq >= 0) {
167
+ flags[key.slice(0, eq)] = key.slice(eq + 1);
168
+ continue;
169
+ }
170
+ if (bools.has(key)) {
171
+ flags[key] = true;
172
+ continue;
173
+ }
174
+ const next = args[i + 1];
175
+ if (next !== undefined && !next.startsWith("-")) {
176
+ flags[key] = next;
177
+ i++;
178
+ }
179
+ else {
180
+ flags[key] = true;
181
+ }
182
+ }
183
+ return { flags, positionals };
184
+ }
72
185
  function readStdin() {
73
186
  return new Promise((resolve, reject) => {
74
187
  let data = "";
@@ -82,30 +195,53 @@ function printHelp() {
82
195
  console.log(`memhook ${VERSION}
83
196
 
84
197
  USAGE
85
- memhook <command>
198
+ memhook <command> [options]
86
199
 
87
200
  COMMANDS
88
201
  run Read Claude Code hook JSON from stdin, emit additionalContext
89
202
  build-catalog Rebuild the memory catalog at $MEMHOOK_CATALOG_PATH
203
+ init Wire memhook into ~/.claude/settings.json (with backup)
204
+ uninstall Remove memhook's hooks from ~/.claude/settings.json
205
+ tail Pretty live view of the routing log (status, latency, memories)
90
206
  version Print version
91
207
  help Show this message
92
208
 
209
+ init OPTIONS
210
+ --provider <p> anthropic | openai | ollama (else prompted; default anthropic)
211
+ --api-key-env <n> env var holding the API key (default per provider)
212
+ --model <m> override the model id
213
+ --bin <name> command written into settings.json (default: memhook)
214
+ --settings <path> settings file to patch (default: ~/.claude/settings.json)
215
+ --no-catalog skip the initial catalog build
216
+ --dry-run print the plan, write nothing
217
+ -y, --yes non-interactive (accept defaults / flags)
218
+
219
+ uninstall OPTIONS
220
+ --settings <path> settings file to patch (default: ~/.claude/settings.json)
221
+ --dry-run print the plan, write nothing
222
+ -y, --yes non-interactive
223
+ --purge also report cache + log locations to clean up
224
+
225
+ tail OPTIONS
226
+ -n, --lines <N> history lines to show before following (default: 10)
227
+ --no-follow print the recent log + summary, then exit (no live follow)
228
+ --status <a,b> only show these statuses (e.g. ok,cache_hit)
229
+ --file <path> log file to read (default: $MEMHOOK_LOG_PATH)
230
+
93
231
  ENV VARS
94
232
  MEMHOOK_ENABLED toggle (default: true)
95
233
  MEMHOOK_PROVIDER anthropic | openai | ollama (default: anthropic)
96
234
  MEMHOOK_MODEL model id (per-provider default if unset)
97
235
  MEMHOOK_API_KEY_ENV env var name holding the API key
98
- (anthropic: ANTHROPIC_API_KEY, openai: OPENAI_API_KEY,
99
- ollama: none required)
100
236
  MEMHOOK_BASE_URL override the provider API endpoint
101
237
  MEMHOOK_CONFIG path to a YAML config file
102
- (default: ~/.config/memhook/config.yaml)
238
+ MEMHOOK_LOG_PATH JSONL log path (read by 'memhook tail')
103
239
  MEMHOOK_MAX_FILES file-count cap (default: 5)
104
240
  MEMHOOK_MAX_ADDITIONAL_CHARS injection size cap (default: 9500)
105
- MEMHOOK_MAX_OUTPUT_TOKENS model output cap (default: 200)
106
241
  MEMHOOK_TIMEOUT_MS request timeout (default: 8000; ollama: 30000)
107
242
  MEMHOOK_DISABLE_CACHE=true skip local LRU cache
108
243
  MEMHOOK_DISABLE_PREFILTER=true skip trivial-prompt skip
244
+ NO_COLOR / MEMHOOK_NO_COLOR disable colour in init/tail output
109
245
  MEMHOOK_DEBUG=true print errors to stderr (default: silent fail-soft)
110
246
 
111
247
  PROVIDERS