memhook 0.2.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +21 -0
  3. package/README.md +204 -0
  4. package/dist/bin/memhook.d.ts +16 -0
  5. package/dist/bin/memhook.d.ts.map +1 -0
  6. package/dist/bin/memhook.js +122 -0
  7. package/dist/bin/memhook.js.map +1 -0
  8. package/dist/src/cache.d.ts +30 -0
  9. package/dist/src/cache.d.ts.map +1 -0
  10. package/dist/src/cache.js +80 -0
  11. package/dist/src/cache.js.map +1 -0
  12. package/dist/src/catalog.d.ts +20 -0
  13. package/dist/src/catalog.d.ts.map +1 -0
  14. package/dist/src/catalog.js +152 -0
  15. package/dist/src/catalog.js.map +1 -0
  16. package/dist/src/config.d.ts +60 -0
  17. package/dist/src/config.d.ts.map +1 -0
  18. package/dist/src/config.js +172 -0
  19. package/dist/src/config.js.map +1 -0
  20. package/dist/src/configFile.d.ts +54 -0
  21. package/dist/src/configFile.d.ts.map +1 -0
  22. package/dist/src/configFile.js +51 -0
  23. package/dist/src/configFile.js.map +1 -0
  24. package/dist/src/index.d.ts +20 -0
  25. package/dist/src/index.d.ts.map +1 -0
  26. package/dist/src/index.js +19 -0
  27. package/dist/src/index.js.map +1 -0
  28. package/dist/src/preFilter.d.ts +16 -0
  29. package/dist/src/preFilter.d.ts.map +1 -0
  30. package/dist/src/preFilter.js +40 -0
  31. package/dist/src/preFilter.js.map +1 -0
  32. package/dist/src/providers/anthropic.d.ts +33 -0
  33. package/dist/src/providers/anthropic.d.ts.map +1 -0
  34. package/dist/src/providers/anthropic.js +98 -0
  35. package/dist/src/providers/anthropic.js.map +1 -0
  36. package/dist/src/providers/factory.d.ts +15 -0
  37. package/dist/src/providers/factory.d.ts.map +1 -0
  38. package/dist/src/providers/factory.js +37 -0
  39. package/dist/src/providers/factory.js.map +1 -0
  40. package/dist/src/providers/http.d.ts +34 -0
  41. package/dist/src/providers/http.d.ts.map +1 -0
  42. package/dist/src/providers/http.js +60 -0
  43. package/dist/src/providers/http.js.map +1 -0
  44. package/dist/src/providers/ollama.d.ts +30 -0
  45. package/dist/src/providers/ollama.d.ts.map +1 -0
  46. package/dist/src/providers/ollama.js +89 -0
  47. package/dist/src/providers/ollama.js.map +1 -0
  48. package/dist/src/providers/openai.d.ts +31 -0
  49. package/dist/src/providers/openai.d.ts.map +1 -0
  50. package/dist/src/providers/openai.js +94 -0
  51. package/dist/src/providers/openai.js.map +1 -0
  52. package/dist/src/providers/types.d.ts +48 -0
  53. package/dist/src/providers/types.d.ts.map +1 -0
  54. package/dist/src/providers/types.js +18 -0
  55. package/dist/src/providers/types.js.map +1 -0
  56. package/dist/src/router.d.ts +32 -0
  57. package/dist/src/router.d.ts.map +1 -0
  58. package/dist/src/router.js +342 -0
  59. package/dist/src/router.js.map +1 -0
  60. package/dist/src/version.d.ts +13 -0
  61. package/dist/src/version.d.ts.map +1 -0
  62. package/dist/src/version.js +13 -0
  63. package/dist/src/version.js.map +1 -0
  64. package/package.json +88 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
1
+ # Changelog
2
+
3
+ All notable changes to memhook are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0](https://github.com/utilia-ai-wox/memhook/compare/v0.1.0-preview.0...v0.2.0) (2026-06-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * **providers:** add multi-provider support and optional YAML config ([97920e4](https://github.com/utilia-ai-wox/memhook/commit/97920e4dc5680dd403336b0a9c44a021fe193de3))
14
+ * **providers:** add OpenAI and Ollama providers + optional YAML config ([8bc5555](https://github.com/utilia-ai-wox/memhook/commit/8bc5555ad73130267984b0d18660a630fdb409d3))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **config:** clamp numeric knobs and widen boolean env vocabulary ([0ad7c6f](https://github.com/utilia-ai-wox/memhook/commit/0ad7c6f13ada7c33933fd8ef70fdf20d35e1f882))
20
+ * **config:** clamp numeric knobs and widen boolean env vocabulary ([ea386c0](https://github.com/utilia-ai-wox/memhook/commit/ea386c0247fc8746908635d0362cff3f01b34be2))
21
+ * point package exports at the compiled dist/src layout ([970ee71](https://github.com/utilia-ai-wox/memhook/commit/970ee711685baaaf63967afb8c5d696a9688486b))
22
+ * point package exports at the compiled dist/src layout ([57e1ff4](https://github.com/utilia-ai-wox/memhook/commit/57e1ff469e4eeb25cbca568f9a390cac690c759a))
23
+
24
+
25
+ ### Documentation
26
+
27
+ * document v0.2 multi-provider and YAML config ([63e6d1b](https://github.com/utilia-ai-wox/memhook/commit/63e6d1b709f2f405b1f18c18958fe28979d57702))
28
+
29
+
30
+ ### Chore
31
+
32
+ * graduate release line to clean 0.2.0 ([f0a07e5](https://github.com/utilia-ai-wox/memhook/commit/f0a07e51fe5792df4efd7af9d59452d97603b675))
33
+
34
+ ## [Unreleased]
35
+
36
+ ### Added
37
+
38
+ - **OpenAI provider** (`MEMHOOK_PROVIDER=openai`) — Chat Completions API,
39
+ `Authorization: Bearer`, catalog as the leading system message so OpenAI's
40
+ automatic prompt caching can engage. Default model `gpt-4o-mini`.
41
+ - **Ollama local provider** (`MEMHOOK_PROVIDER=ollama`) — native `/api/chat`
42
+ endpoint, no API key, `stream:false` + `format:"json"`. Default model
43
+ `llama3.1`, with a 30s default timeout to absorb cold model loads.
44
+ - **YAML config file** (`config.yaml`) — optional, opt-in, read from
45
+ `$MEMHOOK_CONFIG` or `~/.config/memhook/config.yaml`. Precedence is
46
+ env var > YAML > default. A missing or malformed file is ignored
47
+ (fail-soft to defaults). See `config.example.yaml`.
48
+ - `src/providers/factory.ts` — `createProvider()` selects the adapter from
49
+ `config.provider.type` with compile-time exhaustiveness.
50
+ - `src/providers/http.ts` — single shared `postJsonWithRetry` transport
51
+ (timeout + single retry) used by all providers.
52
+ - `MEMHOOK_PROVIDER` and `MEMHOOK_CONFIG` env vars; per-provider defaults for
53
+ model / API-key env var / timeout.
54
+ - Hardening pre-publish (CI, CHANGELOG, CONTRIBUTING, `.env.example`)
55
+
56
+ ### Changed
57
+
58
+ - The provider interface is now provider-agnostic: Anthropic-specific
59
+ `betaHeaders` and `cacheControlTtl` moved off the shared `SelectionRequest`
60
+ into `AnthropicProviderOptions`. `ProviderConfig.apiKey` is now optional
61
+ (local providers need none).
62
+ - Cache key now includes the provider identity (`type:model`) so switching
63
+ provider or model never serves a selection made by a different model.
64
+ - The two hardcoded version strings (`config.ts`, `bin/memhook.ts`) are
65
+ centralised in `src/version.ts`.
66
+
67
+ ### Note
68
+
69
+ - Adding `openai` / `ollama` introduces opt-in outbound calls to
70
+ `api.openai.com` / `localhost:11434`. The default remains Anthropic-only;
71
+ `api.anthropic.com` is still the sole endpoint for an unconfigured user. No
72
+ telemetry, no phone-home.
73
+ - First runtime dependency: `yaml` (zero sub-dependencies).
74
+
75
+ ## [0.1.0-preview.0] — 2026-05-28
76
+
77
+ Initial public preview.
78
+
79
+ ### Added
80
+
81
+ - `src/router.ts` — UserPromptSubmit hook entry point with cap-A1 projection
82
+ fix (skip a file pre-injection if its content would push the cumulated
83
+ injection past `maxAdditionalChars`, while always allowing at least one).
84
+ - `src/catalog.ts` — catalog builder with Q4 title-only reduction for
85
+ non-CWD zones (~50% size cut on a typical 3-repo layout).
86
+ - `src/cache.ts` — local LRU cache keyed on
87
+ `sha256(prompt + catalog_mtime + cwd + script_version)`. Stored as
88
+ per-key JSON files. 60-min TTL by default, 7-day eviction floor.
89
+ - `src/preFilter.ts` — trivial-prompt pre-filter loaded from
90
+ `~/.config/memhook/trivial-words.txt` with a sensible default list.
91
+ - `src/providers/anthropic.ts` — provider implementation for Anthropic
92
+ Messages API. Uses `ephemeral` `1h` cache control on the system prompt
93
+ (GA in 2026, no beta header) so the catalog sits in cache (10× cheaper
94
+ writes amortised across the hour).
95
+ - `src/config.ts` — env-driven config loader. No YAML in v0.1; deferred to
96
+ v0.2.
97
+ - `bin/memhook.ts` — CLI with `run`, `build-catalog`, `version`, `help`.
98
+ - JSONL observability log at `~/.claude/logs/memhook.log` including
99
+ `additional_size_chars` + `additional_size_tokens_est` so users can audit
100
+ the actual size of the injected `additionalContext` over time.
101
+ - 18 unit tests covering the router pipeline, pre-filter normalisation, and
102
+ cache key derivation / TTL / eviction.
103
+
104
+ [Unreleased]: https://github.com/utilia-ai-wox/memhook/compare/v0.1.0-preview.0...HEAD
105
+ [0.1.0-preview.0]: https://github.com/utilia-ai-wox/memhook/releases/tag/v0.1.0-preview.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # memhook
2
+
3
+ > Semantic memory router for Claude Code — picks the relevant feedbacks &
4
+ > rules for each prompt via Haiku, injects them as `additionalContext`.
5
+
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`.
8
+
9
+ ## Why
10
+
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).
15
+
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.
20
+
21
+ ## How it works
22
+
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
+ ```
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ npm install -g memhook # not yet published — see "From source" below
41
+ ```
42
+
43
+ ### From source (preview)
44
+
45
+ ```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
51
+ ```
52
+
53
+ ## Setup
54
+
55
+ 1. **Set your API key**
56
+
57
+ ```bash
58
+ export ANTHROPIC_API_KEY=sk-ant-…
59
+ ```
60
+
61
+ 2. **Build the initial catalog**
62
+
63
+ ```bash
64
+ memhook build-catalog
65
+ # → ~/.claude/cache/memory-catalog.txt
66
+ ```
67
+
68
+ 3. **Wire the hooks** in `~/.claude/settings.json`:
69
+
70
+ ```json
71
+ {
72
+ "hooks": {
73
+ "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "memhook run" }] }],
74
+ "SessionStart": [
75
+ {
76
+ "hooks": [{ "type": "command", "command": "memhook build-catalog" }]
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Configuration
84
+
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.
89
+
90
+ | Variable | Default | Purpose |
91
+ | -------------------------------- | ------------------------------- | -------------------------------------- |
92
+ | `MEMHOOK_ENABLED` | `true` | Master toggle |
93
+ | `MEMHOOK_PROVIDER` | `anthropic` | `anthropic` \| `openai` \| `ollama` |
94
+ | `MEMHOOK_MODEL` | per-provider | Model id (provider default if unset) |
95
+ | `MEMHOOK_API_KEY_ENV` | per-provider | Name of env var holding the API key |
96
+ | `MEMHOOK_BASE_URL` | per-provider | Override the provider API endpoint |
97
+ | `MEMHOOK_CONFIG` | `~/.config/memhook/config.yaml` | Path to the optional YAML config file |
98
+ | `MEMHOOK_MAX_FILES` | `5` | Hard cap on injected files |
99
+ | `MEMHOOK_MAX_ADDITIONAL_CHARS` | `9500` | Soft cap on injected chars (≈2.4k tok) |
100
+ | `MEMHOOK_MAX_OUTPUT_TOKENS` | `200` | Model output cap for the selection |
101
+ | `MEMHOOK_TIMEOUT_MS` | `8000` (`30000` for ollama) | Per-request timeout |
102
+ | `MEMHOOK_DISABLE_CACHE=true` | _(off)_ | Skip local LRU cache |
103
+ | `MEMHOOK_DISABLE_PREFILTER=true` | _(off)_ | Skip trivial-prompt skip |
104
+ | `MEMHOOK_DEBUG=true` | _(off)_ | Print errors to stderr |
105
+
106
+ ### YAML config (optional)
107
+
108
+ memhook works with **no config file at all**. If you prefer YAML, copy
109
+ [`config.example.yaml`](config.example.yaml) to `~/.config/memhook/config.yaml`
110
+ (or point `MEMHOOK_CONFIG` at it). A missing or malformed file is ignored
111
+ silently — memhook falls back to env vars and defaults, never blocking your
112
+ prompt.
113
+
114
+ ```yaml
115
+ provider:
116
+ type: openai
117
+ # model + apiKeyEnv default to gpt-4o-mini + OPENAI_API_KEY
118
+ selection:
119
+ maxFiles: 5
120
+ ```
121
+
122
+ ## Providers
123
+
124
+ The default provider is **Anthropic** — with no `MEMHOOK_PROVIDER` set, the
125
+ only outbound call memhook ever makes is to `api.anthropic.com`, using your own
126
+ key. Selecting another provider is **opt-in** and changes which endpoint is
127
+ contacted. memhook never phones home and has no telemetry; "provider" means the
128
+ LLM endpoint _you_ choose to route through.
129
+
130
+ | Provider | `MEMHOOK_PROVIDER` | Default model | API key | Endpoint |
131
+ | --------- | ------------------ | ------------------ | ------------------- | --------------------------------------- |
132
+ | Anthropic | `anthropic` | `claude-haiku-4-5` | `ANTHROPIC_API_KEY` | `api.anthropic.com` |
133
+ | OpenAI | `openai` | `gpt-4o-mini` | `OPENAI_API_KEY` | `api.openai.com` |
134
+ | Ollama | `ollama` | `llama3.1` | _none_ (local) | `http://localhost:11434` (configurable) |
135
+
136
+ - **OpenAI** — set `MEMHOOK_PROVIDER=openai` and `OPENAI_API_KEY`. Uses the
137
+ Chat Completions API; the catalog rides as the leading system message so
138
+ OpenAI's automatic prompt caching can engage on a large catalog.
139
+ - **Ollama** — set `MEMHOOK_PROVIDER=ollama` and make sure the model is pulled
140
+ (`ollama pull llama3.1`) with the daemon running. No API key required. Hits
141
+ the native `/api/chat` endpoint with `stream:false`; the timeout defaults to
142
+ 30s to absorb cold model loads.
143
+
144
+ ## Observability
145
+
146
+ Every invocation appends one JSON line to `~/.claude/logs/memhook.log`:
147
+
148
+ ```json
149
+ {
150
+ "ts": "2026-05-28T08:41:18Z",
151
+ "prompt_preview": "fix the OPAQUE wire format drift…",
152
+ "selected": ["opaque-interop-rust-ts.md"],
153
+ "latency_ms": 1727,
154
+ "tokens_in": 12,
155
+ "tokens_out": 28,
156
+ "cache_create": 0,
157
+ "cache_read": 13398,
158
+ "additional_size_chars": 20225,
159
+ "additional_size_tokens_est": 5056,
160
+ "status": "ok"
161
+ }
162
+ ```
163
+
164
+ Useful one-liner to inspect the last 7 days:
165
+
166
+ ```bash
167
+ jq -c 'select((.ts | fromdateiso8601) > (now - 7*86400)) | .status' \
168
+ ~/.claude/logs/memhook.log | sort | uniq -c
169
+ ```
170
+
171
+ ## Status values
172
+
173
+ | `status` | Meaning |
174
+ | ---------------------- | ----------------------------------------------------- |
175
+ | `ok` | Files injected from a fresh model selection |
176
+ | `cache_hit` | Files injected from local LRU cache |
177
+ | `pre_filter_skip` | Trivial prompt, LLM call skipped |
178
+ | `empty_selection` | The model returned `[]` (no memory needed) |
179
+ | `all_unfound` | The model returned basenames that don't exist on disk |
180
+ | `no_catalog` | Catalog missing — run `memhook build-catalog` |
181
+ | `no_api_key` | API key env var not set (not needed for ollama) |
182
+ | `provider_init_failed` | Provider couldn't be constructed (bad config) |
183
+ | `api_no_response` | Network error or timeout |
184
+ | `api_no_content` | API returned 200 but no text |
185
+ | `parse_invalid` | Response wasn't a valid JSON array |
186
+
187
+ ## Fail-soft
188
+
189
+ memhook never blocks Claude Code. On any error — missing key, network
190
+ 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.
193
+
194
+ ## Roadmap
195
+
196
+ - `v0.2` ✅ — YAML config file, OpenAI provider, Ollama local provider
197
+ - `v0.3` — TUI live monitor (`memhook tail`)
198
+ - `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
201
+
202
+ ## License
203
+
204
+ MIT © wox
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * memhook CLI — three commands shipped in v0.1 preview:
4
+ *
5
+ * memhook run Read stdin (Claude Code hook JSON), write hook output
6
+ * memhook build-catalog Rebuild ~/.claude/cache/memory-catalog.txt
7
+ * memhook version Print package version
8
+ *
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" }] }]
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=memhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memhook.d.ts","sourceRoot":"","sources":["../../bin/memhook.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * memhook CLI — three commands shipped in v0.1 preview:
4
+ *
5
+ * memhook run Read stdin (Claude Code hook JSON), write hook output
6
+ * memhook build-catalog Rebuild ~/.claude/cache/memory-catalog.txt
7
+ * memhook version Print package version
8
+ *
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" }] }]
14
+ */
15
+ import { route } from "../src/router.js";
16
+ import { buildCatalog } from "../src/catalog.js";
17
+ import { loadConfig } from "../src/config.js";
18
+ import { MEMHOOK_VERSION as VERSION } from "../src/version.js";
19
+ async function main() {
20
+ const cmd = process.argv[2] ?? "help";
21
+ switch (cmd) {
22
+ case "run":
23
+ await cmdRun();
24
+ break;
25
+ case "build-catalog":
26
+ cmdBuildCatalog();
27
+ break;
28
+ case "version":
29
+ case "--version":
30
+ case "-v":
31
+ console.log(VERSION);
32
+ break;
33
+ case "help":
34
+ case "--help":
35
+ case "-h":
36
+ printHelp();
37
+ break;
38
+ default:
39
+ console.error(`memhook: unknown command "${cmd}"`);
40
+ printHelp();
41
+ process.exit(1);
42
+ }
43
+ }
44
+ async function cmdRun() {
45
+ const stdin = await readStdin();
46
+ let output;
47
+ try {
48
+ output = await route(stdin);
49
+ }
50
+ 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
+ if (process.env["MEMHOOK_DEBUG"] === "true") {
59
+ process.stderr.write(`memhook run error: ${String(err)}\n`);
60
+ }
61
+ }
62
+ process.stdout.write(JSON.stringify(output) + "\n");
63
+ }
64
+ function cmdBuildCatalog() {
65
+ const config = loadConfig();
66
+ const result = buildCatalog({
67
+ cwd: process.cwd(),
68
+ outputPath: config.catalog.path,
69
+ });
70
+ process.stderr.write(`[memhook build-catalog] ${config.catalog.path} — ${result.lines}L, ${result.bytes}B\n`);
71
+ }
72
+ function readStdin() {
73
+ return new Promise((resolve, reject) => {
74
+ let data = "";
75
+ process.stdin.setEncoding("utf8");
76
+ process.stdin.on("data", (chunk) => (data += chunk));
77
+ process.stdin.on("end", () => resolve(data));
78
+ process.stdin.on("error", reject);
79
+ });
80
+ }
81
+ function printHelp() {
82
+ console.log(`memhook ${VERSION}
83
+
84
+ USAGE
85
+ memhook <command>
86
+
87
+ COMMANDS
88
+ run Read Claude Code hook JSON from stdin, emit additionalContext
89
+ build-catalog Rebuild the memory catalog at $MEMHOOK_CATALOG_PATH
90
+ version Print version
91
+ help Show this message
92
+
93
+ ENV VARS
94
+ MEMHOOK_ENABLED toggle (default: true)
95
+ MEMHOOK_PROVIDER anthropic | openai | ollama (default: anthropic)
96
+ MEMHOOK_MODEL model id (per-provider default if unset)
97
+ 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
+ MEMHOOK_BASE_URL override the provider API endpoint
101
+ MEMHOOK_CONFIG path to a YAML config file
102
+ (default: ~/.config/memhook/config.yaml)
103
+ MEMHOOK_MAX_FILES file-count cap (default: 5)
104
+ MEMHOOK_MAX_ADDITIONAL_CHARS injection size cap (default: 9500)
105
+ MEMHOOK_MAX_OUTPUT_TOKENS model output cap (default: 200)
106
+ MEMHOOK_TIMEOUT_MS request timeout (default: 8000; ollama: 30000)
107
+ MEMHOOK_DISABLE_CACHE=true skip local LRU cache
108
+ MEMHOOK_DISABLE_PREFILTER=true skip trivial-prompt skip
109
+ MEMHOOK_DEBUG=true print errors to stderr (default: silent fail-soft)
110
+
111
+ PROVIDERS
112
+ Default is Anthropic (only api.anthropic.com is contacted). Selecting
113
+ openai or ollama is opt-in and changes the outbound endpoint to
114
+ api.openai.com or your local Ollama (http://localhost:11434) respectively.
115
+ Per-key precedence is env var > YAML config > built-in default.
116
+ `);
117
+ }
118
+ main().catch((err) => {
119
+ process.stderr.write(`memhook fatal: ${String(err)}\n`);
120
+ process.exit(1);
121
+ });
122
+ //# sourceMappingURL=memhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memhook.js","sourceRoot":"","sources":["../../bin/memhook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local LRU cache for memhook selections.
3
+ *
4
+ * Key = sha256(prompt + catalog_mtime + cwd + script_version + provider). Bump
5
+ * any of these and the entry is automatically invalidated — `provider`
6
+ * (type + model) is included so switching provider/model never serves a stale
7
+ * selection made by a different model.
8
+ *
9
+ * Storage: one JSON file per key under config.cache.dir. TTL enforced via
10
+ * filesystem mtime check.
11
+ */
12
+ export interface CacheKeyInput {
13
+ prompt: string;
14
+ catalogMtimeMs: number;
15
+ cwd: string;
16
+ scriptVersion: string;
17
+ /** Provider identity, e.g. "anthropic:claude-haiku-4-5". */
18
+ provider: string;
19
+ }
20
+ export declare class LocalCache {
21
+ private readonly dir;
22
+ private readonly ttlMin;
23
+ private readonly evictionDays;
24
+ constructor(dir: string, ttlMin: number, evictionDays: number);
25
+ key(input: CacheKeyInput): string;
26
+ get(key: string): string | null;
27
+ put(key: string, value: string): void;
28
+ evictStale(): number;
29
+ }
30
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,UAAU;IAEnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAFZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM;IAKvC,GAAG,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM;IAKjC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAiB/B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAKrC,UAAU,IAAI,MAAM;CAwBrB"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Local LRU cache for memhook selections.
3
+ *
4
+ * Key = sha256(prompt + catalog_mtime + cwd + script_version + provider). Bump
5
+ * any of these and the entry is automatically invalidated — `provider`
6
+ * (type + model) is included so switching provider/model never serves a stale
7
+ * selection made by a different model.
8
+ *
9
+ * Storage: one JSON file per key under config.cache.dir. TTL enforced via
10
+ * filesystem mtime check.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ export class LocalCache {
16
+ dir;
17
+ ttlMin;
18
+ evictionDays;
19
+ constructor(dir, ttlMin, evictionDays) {
20
+ this.dir = dir;
21
+ this.ttlMin = ttlMin;
22
+ this.evictionDays = evictionDays;
23
+ mkdirSync(this.dir, { recursive: true });
24
+ }
25
+ key(input) {
26
+ const raw = `${input.prompt}|${input.catalogMtimeMs}|${input.cwd}|${input.scriptVersion}|${input.provider}`;
27
+ return createHash("sha256").update(raw).digest("hex");
28
+ }
29
+ get(key) {
30
+ const file = join(this.dir, `${key}.json`);
31
+ let stat;
32
+ try {
33
+ stat = statSync(file);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ const ageMs = Date.now() - stat.mtimeMs;
39
+ if (ageMs > this.ttlMin * 60_000)
40
+ return null;
41
+ try {
42
+ return readFileSync(file, "utf8");
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ put(key, value) {
49
+ const file = join(this.dir, `${key}.json`);
50
+ writeFileSync(file, value, "utf8");
51
+ }
52
+ evictStale() {
53
+ let removed = 0;
54
+ const cutoffMs = Date.now() - this.evictionDays * 86_400_000;
55
+ let entries = [];
56
+ try {
57
+ entries = readdirSync(this.dir);
58
+ }
59
+ catch {
60
+ return 0;
61
+ }
62
+ for (const name of entries) {
63
+ if (!name.endsWith(".json"))
64
+ continue;
65
+ const file = join(this.dir, name);
66
+ try {
67
+ const stat = statSync(file);
68
+ if (stat.mtimeMs < cutoffMs) {
69
+ unlinkSync(file);
70
+ removed++;
71
+ }
72
+ }
73
+ catch {
74
+ // ignore
75
+ }
76
+ }
77
+ return removed;
78
+ }
79
+ }
80
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAWjC,MAAM,OAAO,UAAU;IAEF;IACA;IACA;IAHnB,YACmB,GAAW,EACX,MAAc,EACd,YAAoB;QAFpB,QAAG,GAAH,GAAG,CAAQ;QACX,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAQ;QAErC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,KAAoB;QACtB,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC5G,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QAC3C,IAAI,IAAiC,CAAC;QACtC,IAAI,CAAC;YACH,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACxC,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM;YAAE,OAAO,IAAI,CAAC;QAC9C,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAa;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QAC3C,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IAED,UAAU;QACR,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC;QAC7D,IAAI,OAAO,GAAa,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC5B,IAAI,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;oBAC5B,UAAU,CAAC,IAAI,CAAC,CAAC;oBACjB,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Memhook catalog builder — TS port of build-memory-catalog.sh.
3
+ *
4
+ * Discovers feedbacks & projects in `~/.claude/projects/* /memory/`, global
5
+ * rules in `~/.claude/rules/`, and project rules in `<cwd>/.claude/rules/`.
6
+ *
7
+ * Phase 0.5 Q4: title-only for non-CWD zones (~50% catalog size reduction).
8
+ * The CWD zone gets full `basename: description`; others list just basenames.
9
+ */
10
+ export interface CatalogBuildOptions {
11
+ cwd: string;
12
+ projectsRoot?: string;
13
+ globalRulesDir?: string;
14
+ outputPath: string;
15
+ }
16
+ export declare function buildCatalog(opts: CatalogBuildOptions): {
17
+ lines: number;
18
+ bytes: number;
19
+ };
20
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CA2BA"}