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 +23 -72
- package/README.md +130 -68
- package/dist/bin/memhook.d.ts +11 -6
- package/dist/bin/memhook.d.ts.map +1 -1
- package/dist/bin/memhook.js +157 -21
- package/dist/bin/memhook.js.map +1 -1
- package/dist/src/ansi.d.ts +71 -0
- package/dist/src/ansi.d.ts.map +1 -0
- package/dist/src/ansi.js +100 -0
- package/dist/src/ansi.js.map +1 -0
- package/dist/src/cache.d.ts.map +1 -1
- package/dist/src/cache.js +14 -7
- package/dist/src/cache.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.d.ts +47 -0
- package/dist/src/init.d.ts.map +1 -0
- package/dist/src/init.js +283 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/install.d.ts +87 -0
- package/dist/src/install.d.ts.map +1 -0
- package/dist/src/install.js +124 -0
- package/dist/src/install.js.map +1 -0
- package/dist/src/router.d.ts.map +1 -1
- package/dist/src/router.js +34 -7
- package/dist/src/router.js.map +1 -1
- package/dist/src/tail.d.ts +76 -0
- package/dist/src/tail.d.ts.map +1 -0
- package/dist/src/tail.js +280 -0
- package/dist/src/tail.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +5 -1
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
|
-
## [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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: [](https://asciinema.org/a/XXXXX) -->
|
|
20
25
|
|
|
21
|
-
##
|
|
26
|
+
## ✨ Features
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
##
|
|
37
|
+
## 🤔 Why
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
Remove it all later with `memhook uninstall` (also backs up first).
|
|
89
|
+
|
|
90
|
+
</details>
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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` —
|
|
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
|
-
- `
|
|
200
|
-
|
|
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
|
|
package/dist/bin/memhook.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* memhook CLI
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
1
|
+
{"version":3,"file":"memhook.d.ts","sourceRoot":"","sources":["../../bin/memhook.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
|
package/dist/bin/memhook.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* memhook CLI
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
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
|
-
|
|
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
|