memhook 0.4.1 → 0.5.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 +18 -0
- package/README.md +19 -15
- package/dist/bin/memhook.js +36 -1
- package/dist/src/adapters/claudeCode.d.ts +15 -0
- package/dist/src/adapters/claudeCode.js +49 -0
- package/dist/src/adapters/types.d.ts +45 -0
- package/dist/src/adapters/types.js +13 -0
- package/dist/src/catalog.d.ts +16 -2
- package/dist/src/catalog.js +50 -4
- package/dist/src/config.d.ts +46 -0
- package/dist/src/config.js +8 -0
- package/dist/src/configFile.d.ts +9 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -1
- package/dist/src/presetsCmd.d.ts +28 -0
- package/dist/src/presetsCmd.js +94 -0
- package/dist/src/router.d.ts +33 -0
- package/dist/src/router.js +116 -35
- package/dist/src/sources.d.ts +138 -0
- package/dist/src/sources.js +251 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.5.0](https://github.com/utilia-ai-wox/memhook/compare/v0.4.1...v0.5.0) (2026-06-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **catalog:** add built-in host presets ([#41](https://github.com/utilia-ai-wox/memhook/issues/41)) ([596aa11](https://github.com/utilia-ai-wox/memhook/commit/596aa112e691bd94348e0883f12f09950bbe1f99))
|
|
14
|
+
* **catalog:** add custom memory sources ([#40](https://github.com/utilia-ai-wox/memhook/issues/40)) ([fde6507](https://github.com/utilia-ai-wox/memhook/commit/fde6507741f89286c20c31d143cc78eb0f5aff0a))
|
|
15
|
+
* **catalog:** add preset discovery command (presets list/detect) ([#42](https://github.com/utilia-ai-wox/memhook/issues/42)) ([9ce7c1c](https://github.com/utilia-ai-wox/memhook/commit/9ce7c1cb7903c5a7cce204dfdec5809bd1f98b09))
|
|
16
|
+
* **catalog:** add presets:[auto] zero-config preset routing ([#44](https://github.com/utilia-ai-wox/memhook/issues/44)) ([9b6f50a](https://github.com/utilia-ai-wox/memhook/commit/9b6f50ad6eeb1576b4e4da3c0542aeac6589dbd1))
|
|
17
|
+
* **catalog:** omit host-autoloaded rule zones by default ([#39](https://github.com/utilia-ai-wox/memhook/issues/39)) ([0b6dee0](https://github.com/utilia-ai-wox/memhook/commit/0b6dee0a274e497c20070cdea22ecee87e6187ba))
|
|
18
|
+
* **router:** add presets nudge for unrouted host memory ([#43](https://github.com/utilia-ai-wox/memhook/issues/43)) ([f76d159](https://github.com/utilia-ai-wox/memhook/commit/f76d159411e0b65d4c7fcfcfb40bad064700df6a))
|
|
19
|
+
* **router:** introduce a harness-adapter seam with Claude Code as adapter [#1](https://github.com/utilia-ai-wox/memhook/issues/1) ([#38](https://github.com/utilia-ai-wox/memhook/issues/38)) ([cebe646](https://github.com/utilia-ai-wox/memhook/commit/cebe646868ca2f502ffeabe9abf9f9ddc97753a3))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
* clarify the README value proposition ([#36](https://github.com/utilia-ai-wox/memhook/issues/36)) ([f663237](https://github.com/utilia-ai-wox/memhook/commit/f663237de7cd3727db7715faf36309fc83a77fcd))
|
|
25
|
+
|
|
8
26
|
## [0.4.1](https://github.com/utilia-ai-wox/memhook/compare/v0.4.0...v0.4.1) (2026-06-02)
|
|
9
27
|
|
|
10
28
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# memhook
|
|
4
4
|
|
|
5
|
-
**Stop
|
|
5
|
+
**Stop telling Claude to check its memory. memhook auto-injects the notes relevant to each prompt — so it already knows what you told it.**
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
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>
|
|
@@ -14,17 +14,19 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
A semantic memory router for [Claude Code](https://claude.com/claude-code) — a
|
|
17
|
-
`UserPromptSubmit` hook that picks the
|
|
18
|
-
|
|
17
|
+
`UserPromptSubmit` hook that picks the `feedback_*.md` & `rule_*.md` notes relevant
|
|
18
|
+
to _this_ prompt and injects them as `additionalContext`. Your memory gets consulted
|
|
19
|
+
automatically — you stop saying _"go read your memory."_
|
|
19
20
|
|
|
20
21
|
</div>
|
|
21
22
|
|
|
22
23
|
## ✨ Features
|
|
23
24
|
|
|
24
|
-
- 🎯 **
|
|
25
|
-
-
|
|
25
|
+
- 🎯 **Right note, right moment** — auto-selects the 0–5 memory files relevant to _this_ prompt and injects them. No more "go read your memory."
|
|
26
|
+
- 🧠 **Gets better as your memory grows** — relevance is picked per prompt, so a large memory helps instead of drowning the model.
|
|
26
27
|
- 🛡️ **Fail-soft** — never blocks Claude Code; every error path falls back to empty context.
|
|
27
28
|
- 🔌 **Multi-provider** — Anthropic (default), OpenAI, or local Ollama. Your key, your endpoint.
|
|
29
|
+
- 💸 **Light on context** — injects ~2k tokens of signal instead of a 10–14k-token catalog dump.
|
|
28
30
|
- 🤫 **Zero telemetry** — the only outbound call is the LLM endpoint _you_ chose.
|
|
29
31
|
- 🪶 **One dependency** — `yaml`, with zero sub-deps.
|
|
30
32
|
- ⚡ **Cached & pre-filtered** — an LRU cache + a trivial-prompt skip keep latency near zero.
|
|
@@ -35,17 +37,19 @@ files for each prompt and injects them as `additionalContext`.
|
|
|
35
37
|
|
|
36
38
|
Claude Code's `~/.claude/` directory accumulates a growing set of
|
|
37
39
|
`feedback_*.md` (behavioural corrections) and `rule_*.md` (project doctrine)
|
|
38
|
-
files.
|
|
39
|
-
|
|
40
|
+
files. The problem isn't their size — it's that Claude doesn't know what's in
|
|
41
|
+
there: it misses notes that apply, so you keep telling it _"you wrote that
|
|
42
|
+
down, go read it."_
|
|
40
43
|
|
|
41
|
-
memhook
|
|
42
|
-
prompt against a one-line catalog of all your memory files
|
|
43
|
-
the
|
|
44
|
+
memhook removes that chore. A cheap router model (**Haiku 4.5** by default)
|
|
45
|
+
matches each prompt against a one-line catalog of all your memory files and
|
|
46
|
+
injects just the relevant ones — so the right note is already in context,
|
|
47
|
+
automatically. The rest sit on disk, invisible until they matter.
|
|
44
48
|
|
|
45
|
-
| Approach | Tokens / prompt |
|
|
46
|
-
| --------------------- |
|
|
47
|
-
| Load all memory files | 10–14k |
|
|
48
|
-
| **memhook** |
|
|
49
|
+
| Approach | What Claude sees | Tokens / prompt |
|
|
50
|
+
| --------------------- | ------------------------------- | --------------- |
|
|
51
|
+
| Load all memory files | mostly irrelevant noise | 10–14k |
|
|
52
|
+
| **memhook** | only what matches _this_ prompt | ~2k |
|
|
49
53
|
|
|
50
54
|
## 🚀 Quick start
|
|
51
55
|
|
|
@@ -282,7 +286,7 @@ non-negotiable; the [`failsoft-auditor`](.claude/agents/failsoft-auditor.md)
|
|
|
282
286
|
agent guards it on every PR.
|
|
283
287
|
|
|
284
288
|
> [!TIP]
|
|
285
|
-
> ⭐ If memhook
|
|
289
|
+
> ⭐ If memhook keeps Claude on-context without the "go read your memory" nudges, **star the repo** — it helps other Claude Code users find it.
|
|
286
290
|
|
|
287
291
|
## License
|
|
288
292
|
|
package/dist/bin/memhook.js
CHANGED
|
@@ -17,16 +17,21 @@
|
|
|
17
17
|
* "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "memhook run" }] }]
|
|
18
18
|
* "SessionStart": [{ "hooks": [{ "type": "command", "command": "memhook build-catalog" }] }]
|
|
19
19
|
*/
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { readdirSync } from "node:fs";
|
|
20
22
|
import { route } from "../src/router.js";
|
|
21
23
|
import { buildCatalog } from "../src/catalog.js";
|
|
22
24
|
import { loadConfig } from "../src/config.js";
|
|
25
|
+
import { resolveSources } from "../src/sources.js";
|
|
23
26
|
import { runInit, runUninstall } from "../src/init.js";
|
|
24
27
|
import { runTail } from "../src/tail.js";
|
|
25
28
|
import { runSkills } from "../src/skillsCmd.js";
|
|
29
|
+
import { runPresets } from "../src/presetsCmd.js";
|
|
26
30
|
import { isCompanionSkill } from "../src/skills.js";
|
|
27
31
|
import { MEMHOOK_VERSION as VERSION } from "../src/version.js";
|
|
28
32
|
const PROVIDERS = ["anthropic", "openai", "ollama"];
|
|
29
33
|
const SKILLS_SUBCOMMANDS = ["install", "uninstall", "list"];
|
|
34
|
+
const PRESETS_SUBCOMMANDS = ["list", "detect"];
|
|
30
35
|
async function main() {
|
|
31
36
|
const cmd = process.argv[2] ?? "help";
|
|
32
37
|
const args = process.argv.slice(3);
|
|
@@ -49,6 +54,9 @@ async function main() {
|
|
|
49
54
|
case "skills":
|
|
50
55
|
process.exitCode = await cmdSkills(args);
|
|
51
56
|
break;
|
|
57
|
+
case "presets":
|
|
58
|
+
process.exitCode = cmdPresets(args);
|
|
59
|
+
break;
|
|
52
60
|
case "version":
|
|
53
61
|
case "--version":
|
|
54
62
|
case "-v":
|
|
@@ -88,9 +96,12 @@ async function cmdRun() {
|
|
|
88
96
|
}
|
|
89
97
|
function cmdBuildCatalog() {
|
|
90
98
|
const config = loadConfig();
|
|
99
|
+
const cwd = process.cwd();
|
|
91
100
|
const result = buildCatalog({
|
|
92
|
-
cwd
|
|
101
|
+
cwd,
|
|
93
102
|
outputPath: config.catalog.path,
|
|
103
|
+
resurfaceHostLoaded: config.resurfaceHostLoaded,
|
|
104
|
+
customSources: resolveSources(config.customSources, config.presets, cwd, homedir(), readdirSync),
|
|
94
105
|
});
|
|
95
106
|
process.stderr.write(`[memhook build-catalog] ${config.catalog.path} — ${result.lines}L, ${result.bytes}B\n`);
|
|
96
107
|
}
|
|
@@ -144,6 +155,15 @@ async function cmdSkills(args) {
|
|
|
144
155
|
force: flags["force"] === true,
|
|
145
156
|
});
|
|
146
157
|
}
|
|
158
|
+
function cmdPresets(args) {
|
|
159
|
+
const { positionals } = parseArgs(args, BOOL_PRESETS);
|
|
160
|
+
const sub = positionals[0] ?? "detect";
|
|
161
|
+
if (!PRESETS_SUBCOMMANDS.includes(sub)) {
|
|
162
|
+
process.stderr.write(`memhook presets: unknown subcommand "${sub}" (list | detect)\n`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
return runPresets({ subcommand: sub });
|
|
166
|
+
}
|
|
147
167
|
async function cmdUninstall(args) {
|
|
148
168
|
const { flags } = parseArgs(args, BOOL_UNINSTALL);
|
|
149
169
|
return runUninstall({
|
|
@@ -176,6 +196,7 @@ const BOOL_INIT = new Set(["yes", "dry-run", "no-catalog", "skills", "no-skills"
|
|
|
176
196
|
const BOOL_UNINSTALL = new Set(["yes", "dry-run", "purge"]);
|
|
177
197
|
const BOOL_TAIL = new Set(["no-follow"]);
|
|
178
198
|
const BOOL_SKILLS = new Set(["yes", "dry-run", "force"]);
|
|
199
|
+
const BOOL_PRESETS = new Set([]);
|
|
179
200
|
const SHORT = { "-y": "--yes", "-n": "--lines" };
|
|
180
201
|
function strFlag(v) {
|
|
181
202
|
return typeof v === "string" ? v : undefined;
|
|
@@ -240,6 +261,7 @@ COMMANDS
|
|
|
240
261
|
uninstall Remove memhook's hooks from ~/.claude/settings.json
|
|
241
262
|
tail Pretty live view of the routing log (status, latency, memories)
|
|
242
263
|
skills Install/uninstall/list companion skills (/wrap /curate /relay)
|
|
264
|
+
presets List/detect built-in per-host source presets (experimental)
|
|
243
265
|
version Print version
|
|
244
266
|
help Show this message
|
|
245
267
|
|
|
@@ -275,6 +297,11 @@ skills SUBCOMMANDS
|
|
|
275
297
|
--dry-run print the plan, write nothing
|
|
276
298
|
-y, --yes non-interactive (accept defaults)
|
|
277
299
|
|
|
300
|
+
presets SUBCOMMANDS
|
|
301
|
+
detect scan this project + home for host memory, print the
|
|
302
|
+
'presets: […]' snippet to enable what's found (default)
|
|
303
|
+
list show every built-in preset (cline | continue | copilot | windsurf)
|
|
304
|
+
|
|
278
305
|
ENV VARS
|
|
279
306
|
MEMHOOK_ENABLED toggle (default: true)
|
|
280
307
|
MEMHOOK_PROVIDER anthropic | openai | ollama (default: anthropic)
|
|
@@ -288,10 +315,18 @@ ENV VARS
|
|
|
288
315
|
MEMHOOK_TIMEOUT_MS request timeout (default: 8000; ollama: 30000)
|
|
289
316
|
MEMHOOK_DISABLE_CACHE=true skip local LRU cache
|
|
290
317
|
MEMHOOK_DISABLE_PREFILTER=true skip trivial-prompt skip
|
|
318
|
+
MEMHOOK_RESURFACE_HOST_LOADED route rules the host already auto-loads at
|
|
319
|
+
launch (default: false). Enable for long
|
|
320
|
+
sessions / no-drift projects (re-surfaces
|
|
321
|
+
launch-loaded rules near the prompt).
|
|
291
322
|
MEMHOOK_CURATE_NUDGE /curate-nudge toggle (default: true)
|
|
292
323
|
MEMHOOK_CURATE_NUDGE_TOKENS catalog-token threshold to nudge (default: 15000)
|
|
293
324
|
MEMHOOK_CURATE_NUDGE_FILES memory-file-count threshold to nudge (default: 250)
|
|
294
325
|
MEMHOOK_CURATE_NUDGE_COOLDOWN_DAYS min days between nudges (default: 7)
|
|
326
|
+
MEMHOOK_PRESETS_NUDGE presets-nudge toggle (default: true) — suggests
|
|
327
|
+
'memhook presets detect' when a known host's
|
|
328
|
+
memory exists in the project but isn't routed
|
|
329
|
+
MEMHOOK_PRESETS_NUDGE_COOLDOWN_DAYS min days between presets nudges (default: 7)
|
|
295
330
|
NO_COLOR / MEMHOOK_NO_COLOR disable colour in init/tail output
|
|
296
331
|
MEMHOOK_DEBUG=true print errors to stderr (default: silent fail-soft)
|
|
297
332
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code adapter — memhook's first and reference harness adapter.
|
|
3
|
+
*
|
|
4
|
+
* Input (stdin): the `UserPromptSubmit` hook JSON; only `prompt` + `cwd` are
|
|
5
|
+
* used (docs/SPECIFICATION.md §10.1).
|
|
6
|
+
* Output (stdout): `{ hookSpecificOutput: { hookEventName: "UserPromptSubmit",
|
|
7
|
+
* additionalContext }, systemMessage? }` (§10.2). `systemMessage` is emitted
|
|
8
|
+
* only when the result carries one (the `/curate` nudge); absent otherwise, so
|
|
9
|
+
* the serialised shape is byte-identical to memhook's pre-adapter output.
|
|
10
|
+
*
|
|
11
|
+
* Contract source: https://code.claude.com/docs/en/hooks (mirrored in §10).
|
|
12
|
+
*/
|
|
13
|
+
import type { HookOutput } from "../router.js";
|
|
14
|
+
import type { HarnessAdapter } from "./types.js";
|
|
15
|
+
export declare const claudeCodeAdapter: HarnessAdapter<HookOutput>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code adapter — memhook's first and reference harness adapter.
|
|
3
|
+
*
|
|
4
|
+
* Input (stdin): the `UserPromptSubmit` hook JSON; only `prompt` + `cwd` are
|
|
5
|
+
* used (docs/SPECIFICATION.md §10.1).
|
|
6
|
+
* Output (stdout): `{ hookSpecificOutput: { hookEventName: "UserPromptSubmit",
|
|
7
|
+
* additionalContext }, systemMessage? }` (§10.2). `systemMessage` is emitted
|
|
8
|
+
* only when the result carries one (the `/curate` nudge); absent otherwise, so
|
|
9
|
+
* the serialised shape is byte-identical to memhook's pre-adapter output.
|
|
10
|
+
*
|
|
11
|
+
* Contract source: https://code.claude.com/docs/en/hooks (mirrored in §10).
|
|
12
|
+
*/
|
|
13
|
+
export const claudeCodeAdapter = {
|
|
14
|
+
id: "claude-code",
|
|
15
|
+
parseInput(stdinJson) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(stdinJson);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// Guard against non-object payloads (`"null"`, a bare number/string): they
|
|
24
|
+
// are not usable hook inputs. This keeps `route()` from throwing on them —
|
|
25
|
+
// a strict tightening of fail-soft, with the same empty output as before.
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
27
|
+
return null;
|
|
28
|
+
const obj = parsed;
|
|
29
|
+
if (typeof obj.prompt !== "string")
|
|
30
|
+
return null;
|
|
31
|
+
const input = { prompt: obj.prompt };
|
|
32
|
+
// exactOptionalPropertyTypes: only set cwd when the host actually sent one.
|
|
33
|
+
if (typeof obj.cwd === "string")
|
|
34
|
+
input.cwd = obj.cwd;
|
|
35
|
+
return input;
|
|
36
|
+
},
|
|
37
|
+
formatOutput(result) {
|
|
38
|
+
const output = {
|
|
39
|
+
hookSpecificOutput: {
|
|
40
|
+
hookEventName: "UserPromptSubmit",
|
|
41
|
+
additionalContext: result.additionalContext,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
if (result.systemMessage !== undefined) {
|
|
45
|
+
output.systemMessage = result.systemMessage;
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* memhook's selection pipeline is harness-agnostic: it consumes a normalised
|
|
5
|
+
* `{prompt, cwd}` and produces a `RouteResult`. The ONLY harness-specific
|
|
6
|
+
* surface is (a) parsing the host's hook stdin into that normalised input and
|
|
7
|
+
* (b) serialising the result into the host's stdout envelope. A `HarnessAdapter`
|
|
8
|
+
* captures exactly those two ends, so a new host (Codex, Gemini, …) is an
|
|
9
|
+
* adapter, not a fork of the router.
|
|
10
|
+
*
|
|
11
|
+
* See docs/SPECIFICATION.md §5 (architecture) and §10 (hook contract).
|
|
12
|
+
*/
|
|
13
|
+
/** Normalised hook input. Every adapter maps its host's stdin to this shape. */
|
|
14
|
+
export interface HarnessInput {
|
|
15
|
+
/** The verbatim user prompt. */
|
|
16
|
+
prompt: string;
|
|
17
|
+
/** The project working directory, when the host provides one. */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Harness-agnostic outcome of one routing pass. Adapters serialise this into
|
|
22
|
+
* their host's stdout envelope.
|
|
23
|
+
*/
|
|
24
|
+
export interface RouteResult {
|
|
25
|
+
/** Context to inject ahead of the user prompt. Empty string = inject nothing. */
|
|
26
|
+
additionalContext: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional one-line notice to the user (the `/curate` nudge). Absent on every
|
|
29
|
+
* normal turn, so a host whose envelope has no equivalent simply ignores it.
|
|
30
|
+
*/
|
|
31
|
+
systemMessage?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The host-specific surface of the hook. `parseInput` reads the host's stdin
|
|
35
|
+
* JSON into a `HarnessInput` (or `null` when it isn't a usable hook input);
|
|
36
|
+
* `formatOutput` serialises a `RouteResult` into the host's stdout shape.
|
|
37
|
+
*/
|
|
38
|
+
export interface HarnessAdapter<TOutput = unknown> {
|
|
39
|
+
/** Stable identifier, e.g. `"claude-code"`. */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** Parse the host's hook stdin JSON into `{prompt, cwd}`, or `null` if unusable. */
|
|
42
|
+
parseInput(stdinJson: string): HarnessInput | null;
|
|
43
|
+
/** Serialise a routing result into the host's stdout envelope. */
|
|
44
|
+
formatOutput(result: RouteResult): TOutput;
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* memhook's selection pipeline is harness-agnostic: it consumes a normalised
|
|
5
|
+
* `{prompt, cwd}` and produces a `RouteResult`. The ONLY harness-specific
|
|
6
|
+
* surface is (a) parsing the host's hook stdin into that normalised input and
|
|
7
|
+
* (b) serialising the result into the host's stdout envelope. A `HarnessAdapter`
|
|
8
|
+
* captures exactly those two ends, so a new host (Codex, Gemini, …) is an
|
|
9
|
+
* adapter, not a fork of the router.
|
|
10
|
+
*
|
|
11
|
+
* See docs/SPECIFICATION.md §5 (architecture) and §10 (hook contract).
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/src/catalog.d.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memhook catalog builder — TS port of build-memory-catalog.sh.
|
|
3
3
|
*
|
|
4
|
-
* Discovers feedbacks & projects in `~/.claude/projects/* /memory
|
|
5
|
-
* rules
|
|
4
|
+
* Discovers feedbacks & projects in `~/.claude/projects/* /memory/`. Global
|
|
5
|
+
* rules (`~/.claude/rules/`) and project rules (`<cwd>/.claude/rules/`) are
|
|
6
|
+
* host-autoloaded and included ONLY when `resurfaceHostLoaded` is set (default
|
|
7
|
+
* off), so memhook does not re-catalogue what Claude Code already loads at
|
|
8
|
+
* launch.
|
|
6
9
|
*
|
|
7
10
|
* Phase 0.5 Q4: title-only for non-CWD zones (~50% catalog size reduction).
|
|
8
11
|
* The CWD zone gets full `basename: description`; others list just basenames.
|
|
9
12
|
*/
|
|
13
|
+
import { type CustomSource } from "./sources.js";
|
|
10
14
|
export interface CatalogBuildOptions {
|
|
11
15
|
cwd: string;
|
|
12
16
|
projectsRoot?: string;
|
|
13
17
|
globalRulesDir?: string;
|
|
14
18
|
outputPath: string;
|
|
19
|
+
/**
|
|
20
|
+
* Include the host-autoloaded rule zones (`~/.claude/rules`,
|
|
21
|
+
* `<cwd>/.claude/rules`) in the catalog. Default `false`: those files are
|
|
22
|
+
* loaded in full by Claude Code at launch, so cataloguing them would let the
|
|
23
|
+
* router re-inject what the host already has (double-injection). See
|
|
24
|
+
* `MemhookConfig.resurfaceHostLoaded`.
|
|
25
|
+
*/
|
|
26
|
+
resurfaceHostLoaded?: boolean;
|
|
27
|
+
/** Extra `.md` source dirs to catalog alongside the built-in zones. */
|
|
28
|
+
customSources?: CustomSource[];
|
|
15
29
|
}
|
|
16
30
|
export declare function buildCatalog(opts: CatalogBuildOptions): {
|
|
17
31
|
lines: number;
|
package/dist/src/catalog.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memhook catalog builder — TS port of build-memory-catalog.sh.
|
|
3
3
|
*
|
|
4
|
-
* Discovers feedbacks & projects in `~/.claude/projects/* /memory
|
|
5
|
-
* rules
|
|
4
|
+
* Discovers feedbacks & projects in `~/.claude/projects/* /memory/`. Global
|
|
5
|
+
* rules (`~/.claude/rules/`) and project rules (`<cwd>/.claude/rules/`) are
|
|
6
|
+
* host-autoloaded and included ONLY when `resurfaceHostLoaded` is set (default
|
|
7
|
+
* off), so memhook does not re-catalogue what Claude Code already loads at
|
|
8
|
+
* launch.
|
|
6
9
|
*
|
|
7
10
|
* Phase 0.5 Q4: title-only for non-CWD zones (~50% catalog size reduction).
|
|
8
11
|
* The CWD zone gets full `basename: description`; others list just basenames.
|
|
@@ -10,6 +13,7 @@
|
|
|
10
13
|
import { existsSync, readFileSync, readdirSync, writeFileSync, statSync, renameSync, } from "node:fs";
|
|
11
14
|
import { join, basename as pathBasename } from "node:path";
|
|
12
15
|
import { homedir } from "node:os";
|
|
16
|
+
import { activeCustomSources, listMatchingMdFiles } from "./sources.js";
|
|
13
17
|
export function buildCatalog(opts) {
|
|
14
18
|
const home = homedir();
|
|
15
19
|
const projectsRoot = opts.projectsRoot ?? join(home, ".claude", "projects");
|
|
@@ -18,8 +22,22 @@ export function buildCatalog(opts) {
|
|
|
18
22
|
const sections = [];
|
|
19
23
|
sections.push(emitMemorySection("feedback", "MEMORY FEEDBACKS", memoryDirs));
|
|
20
24
|
sections.push(emitMemorySection("project", "MEMORY PROJECTS", memoryDirs));
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// Global + project rules are host-autoloaded: Claude Code reads
|
|
26
|
+
// `~/.claude/rules/*.md` and `<cwd>/.claude/rules/*.md` in full at launch. By
|
|
27
|
+
// default memhook leaves them OUT of the catalog so the router never re-routes
|
|
28
|
+
// what the host already loaded (no double-injection — it routes only the
|
|
29
|
+
// not-autoloaded `feedback_*/project_*` memory). `resurfaceHostLoaded` adds
|
|
30
|
+
// them back for positional re-surfacing (long sessions / no-drift projects).
|
|
31
|
+
if (opts.resurfaceHostLoaded) {
|
|
32
|
+
sections.push(emitRulesSection("GLOBAL RULES", globalRulesDir, true));
|
|
33
|
+
sections.push(emitRulesSection(`PROJECT RULES (${pathBasename(opts.cwd)})`, join(opts.cwd, ".claude", "rules"), true));
|
|
34
|
+
}
|
|
35
|
+
// User-declared extra sources (cable onto existing project memory). Skipped
|
|
36
|
+
// when host-autoloaded unless resurfaceHostLoaded — same gate as the rules.
|
|
37
|
+
const custom = activeCustomSources(opts.customSources ?? [], opts.resurfaceHostLoaded ?? false);
|
|
38
|
+
if (custom.length > 0) {
|
|
39
|
+
sections.push(emitCustomSourcesSection(custom));
|
|
40
|
+
}
|
|
23
41
|
const content = sections.join("\n");
|
|
24
42
|
const tmp = `${opts.outputPath}.tmp.${process.pid}`;
|
|
25
43
|
writeFileSync(tmp, content, "utf8");
|
|
@@ -106,6 +124,34 @@ function emitRulesSection(label, dir, isCwdZone) {
|
|
|
106
124
|
lines.push("");
|
|
107
125
|
return lines.join("\n");
|
|
108
126
|
}
|
|
127
|
+
function emitCustomSourcesSection(sources) {
|
|
128
|
+
const lines = ["=== CUSTOM SOURCES ==="];
|
|
129
|
+
let total = 0;
|
|
130
|
+
for (const src of sources) {
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = readdirSync(src.dir);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
lines.push(`--- ${src.dir} (directory not found) ---`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Only `.md` files can be injected (router SAFE_BASENAME_RE), so the catalog
|
|
140
|
+
// lists only those — a glob like `*.txt` simply yields nothing. Shared with
|
|
141
|
+
// preset detection via listMatchingMdFiles so the two never disagree.
|
|
142
|
+
const files = listMatchingMdFiles(entries, src.glob);
|
|
143
|
+
if (files.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
lines.push(`--- ${src.dir} ---`);
|
|
146
|
+
for (const f of files) {
|
|
147
|
+
lines.push(`${f}: ${extractDescription(join(src.dir, f))}`);
|
|
148
|
+
}
|
|
149
|
+
total += files.length;
|
|
150
|
+
}
|
|
151
|
+
lines.push(`(${total} entries)`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
109
155
|
function listMemoryFiles(dir, prefix) {
|
|
110
156
|
let entries = [];
|
|
111
157
|
try {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* try-boundaries on some paths). All YAML I/O is isolated in `loadYamlConfig`,
|
|
12
12
|
* which swallows every error to null.
|
|
13
13
|
*/
|
|
14
|
+
import { type CustomSource } from "./sources.js";
|
|
14
15
|
export type ProviderType = "anthropic" | "openai" | "ollama";
|
|
15
16
|
export interface MemhookConfig {
|
|
16
17
|
enabled: boolean;
|
|
@@ -51,6 +52,38 @@ export interface MemhookConfig {
|
|
|
51
52
|
path: string;
|
|
52
53
|
};
|
|
53
54
|
searchDirs: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Whether to route memory the HOST already auto-loads at launch. Claude Code
|
|
57
|
+
* loads `~/.claude/rules/*.md` and `<cwd>/.claude/rules/*.md` in full at
|
|
58
|
+
* startup, so routing them again is positional re-surfacing, not new recall.
|
|
59
|
+
*
|
|
60
|
+
* OFF by default (the clean public behaviour): the catalog omits those
|
|
61
|
+
* host-autoloaded rule zones, so memhook routes only memory the host does NOT
|
|
62
|
+
* load (the `feedback_` / `project_` zones) — no double-injection. Measured: on
|
|
63
|
+
* a real 4.8k-prompt corpus, ~20% of injecting prompts re-injected a rule the
|
|
64
|
+
* host had already loaded in full (docs/private POC, 2026-06-02).
|
|
65
|
+
*
|
|
66
|
+
* Turn ON (`MEMHOOK_RESURFACE_HOST_LOADED=true`) for **long sessions / long
|
|
67
|
+
* context** — launch-loaded rules drift far from the current prompt, so
|
|
68
|
+
* re-surfacing the relevant ones near it restores their salience — or a
|
|
69
|
+
* **complex project that tolerates no drift**, where the redundant re-injection
|
|
70
|
+
* is a deliberate guard-rail.
|
|
71
|
+
*/
|
|
72
|
+
resurfaceHostLoaded: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Extra memory sources beyond the built-in `~/.claude` zones — directories of
|
|
75
|
+
* `.md` files (any naming, via a glob) that memhook catalogs + routes like its
|
|
76
|
+
* own zones. This is how memhook cables onto memory that already exists in a
|
|
77
|
+
* project. YAML-only (`customSources:`), default empty. See `src/sources.ts`.
|
|
78
|
+
*/
|
|
79
|
+
customSources: CustomSource[];
|
|
80
|
+
/**
|
|
81
|
+
* Enabled built-in host presets (e.g. `continue`, `cline`) — named bundles of
|
|
82
|
+
* sources for a known tool's `.md` convention, expanded against cwd/home at
|
|
83
|
+
* catalog/router time. YAML-only (`presets:`), default empty. All presets are
|
|
84
|
+
* experimental (doc-verified, not live-tested). See `src/sources.ts`.
|
|
85
|
+
*/
|
|
86
|
+
presets: string[];
|
|
54
87
|
logging: {
|
|
55
88
|
jsonlPath: string;
|
|
56
89
|
};
|
|
@@ -65,6 +98,19 @@ export interface MemhookConfig {
|
|
|
65
98
|
thresholdFiles: number;
|
|
66
99
|
cooldownDays: number;
|
|
67
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Optional proactive nudge: when a known host's memory directory exists in the
|
|
103
|
+
* project but no matching `presets:` entry routes it yet, the router attaches a
|
|
104
|
+
* one-line `systemMessage` suggesting `memhook presets detect`. Local-only (a
|
|
105
|
+
* readdir per preset dir, gated behind the cooldown), best-effort (never
|
|
106
|
+
* affects fail-soft), and always opt-in — it only points at the explicit
|
|
107
|
+
* `presets:` config, never auto-routes. See `maybePresetsNudge` in
|
|
108
|
+
* `src/router.ts`.
|
|
109
|
+
*/
|
|
110
|
+
presetsNudge: {
|
|
111
|
+
enabled: boolean;
|
|
112
|
+
cooldownDays: number;
|
|
113
|
+
};
|
|
68
114
|
scriptVersion: string;
|
|
69
115
|
}
|
|
70
116
|
export declare function loadConfig(env?: NodeJS.ProcessEnv): MemhookConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { loadYamlConfig } from "./configFile.js";
|
|
17
|
+
import { resolveCustomSources, resolvePresetNames } from "./sources.js";
|
|
17
18
|
import { MEMHOOK_VERSION } from "./version.js";
|
|
18
19
|
/** Per-provider defaults applied once the provider type is known. */
|
|
19
20
|
const PROVIDER_DEFAULTS = {
|
|
@@ -163,6 +164,9 @@ export function loadConfig(env = process.env) {
|
|
|
163
164
|
str("MEMHOOK_PROJECTS_ROOT", yaml?.searchDirs?.projectsRoot, join(home, ".claude", "projects")),
|
|
164
165
|
str("MEMHOOK_GLOBAL_RULES_DIR", yaml?.searchDirs?.globalRulesDir, join(home, ".claude", "rules")),
|
|
165
166
|
],
|
|
167
|
+
resurfaceHostLoaded: bool("MEMHOOK_RESURFACE_HOST_LOADED", yaml?.resurfaceHostLoaded, false),
|
|
168
|
+
customSources: resolveCustomSources(yaml?.customSources, home),
|
|
169
|
+
presets: resolvePresetNames(yaml?.presets),
|
|
166
170
|
logging: {
|
|
167
171
|
jsonlPath: str("MEMHOOK_LOG_PATH", yaml?.logging?.jsonlPath, join(home, ".claude", "logs", "memhook.log")),
|
|
168
172
|
},
|
|
@@ -172,6 +176,10 @@ export function loadConfig(env = process.env) {
|
|
|
172
176
|
thresholdFiles: num("MEMHOOK_CURATE_NUDGE_FILES", yaml?.curateNudge?.thresholdFiles, 250),
|
|
173
177
|
cooldownDays: num("MEMHOOK_CURATE_NUDGE_COOLDOWN_DAYS", yaml?.curateNudge?.cooldownDays, 7),
|
|
174
178
|
},
|
|
179
|
+
presetsNudge: {
|
|
180
|
+
enabled: bool("MEMHOOK_PRESETS_NUDGE", yaml?.presetsNudge?.enabled, true),
|
|
181
|
+
cooldownDays: num("MEMHOOK_PRESETS_NUDGE_COOLDOWN_DAYS", yaml?.presetsNudge?.cooldownDays, 7),
|
|
182
|
+
},
|
|
175
183
|
scriptVersion: MEMHOOK_VERSION,
|
|
176
184
|
};
|
|
177
185
|
}
|
package/dist/src/configFile.d.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface RawConfigFile {
|
|
|
45
45
|
projectsRoot?: string;
|
|
46
46
|
globalRulesDir?: string;
|
|
47
47
|
};
|
|
48
|
+
resurfaceHostLoaded?: boolean;
|
|
49
|
+
/** Extra `.md` source dirs; validated + narrowed by `resolveCustomSources`. */
|
|
50
|
+
customSources?: unknown;
|
|
51
|
+
/** Built-in host preset names; validated by `resolvePresetNames`. */
|
|
52
|
+
presets?: unknown;
|
|
48
53
|
logging?: {
|
|
49
54
|
jsonlPath?: string;
|
|
50
55
|
};
|
|
@@ -54,6 +59,10 @@ export interface RawConfigFile {
|
|
|
54
59
|
thresholdFiles?: number;
|
|
55
60
|
cooldownDays?: number;
|
|
56
61
|
};
|
|
62
|
+
presetsNudge?: {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
cooldownDays?: number;
|
|
65
|
+
};
|
|
57
66
|
}
|
|
58
67
|
export declare function resolveConfigPath(env: NodeJS.ProcessEnv): string;
|
|
59
68
|
export declare function loadYamlConfig(env: NodeJS.ProcessEnv): RawConfigFile | null;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { loadConfig, type MemhookConfig, type ProviderType } from "./config.js";
|
|
9
9
|
export { loadYamlConfig, resolveConfigPath, type RawConfigFile } from "./configFile.js";
|
|
10
|
-
export { route, type HookInput, type HookOutput } from "./router.js";
|
|
10
|
+
export { route, runHarness, type HookInput, type HookOutput } from "./router.js";
|
|
11
|
+
export { claudeCodeAdapter } from "./adapters/claudeCode.js";
|
|
12
|
+
export type { HarnessAdapter, HarnessInput, RouteResult } from "./adapters/types.js";
|
|
11
13
|
export { buildCatalog, type CatalogBuildOptions } from "./catalog.js";
|
|
14
|
+
export { resolveCustomSources, activeCustomSources, resolveSources, expandPresets, resolveActivePresetNames, resolvePresetNames, isPresetName, globToRegExp, expandHome, HOST_PRESETS, PRESET_NAMES, PRESET_AUTO, type CustomSource, type SourceScope, type PresetDef, } from "./sources.js";
|
|
12
15
|
export { LocalCache, type CacheKeyInput } from "./cache.js";
|
|
13
16
|
export { PreFilter } from "./preFilter.js";
|
|
14
17
|
export { MEMHOOK_VERSION } from "./version.js";
|
package/dist/src/index.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { loadConfig } from "./config.js";
|
|
9
9
|
export { loadYamlConfig, resolveConfigPath } from "./configFile.js";
|
|
10
|
-
export { route } from "./router.js";
|
|
10
|
+
export { route, runHarness } from "./router.js";
|
|
11
|
+
export { claudeCodeAdapter } from "./adapters/claudeCode.js";
|
|
11
12
|
export { buildCatalog } from "./catalog.js";
|
|
13
|
+
export { resolveCustomSources, activeCustomSources, resolveSources, expandPresets, resolveActivePresetNames, resolvePresetNames, isPresetName, globToRegExp, expandHome, HOST_PRESETS, PRESET_NAMES, PRESET_AUTO, } from "./sources.js";
|
|
12
14
|
export { LocalCache } from "./cache.js";
|
|
13
15
|
export { PreFilter } from "./preFilter.js";
|
|
14
16
|
export { MEMHOOK_VERSION } from "./version.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memhook presets list|detect` — discover the built-in per-host source presets
|
|
3
|
+
* so the user never has to know a preset's name or hand-write its paths.
|
|
4
|
+
*
|
|
5
|
+
* list Show every built-in preset (name, summary, the dirs/globs it points
|
|
6
|
+
* at). Static — no disk access.
|
|
7
|
+
* detect Scan this project (cwd) + the home dir for the preset directories
|
|
8
|
+
* that actually hold matching `.md` files, then print the YAML snippet
|
|
9
|
+
* (`presets: [...]`) that enables the ones found.
|
|
10
|
+
*
|
|
11
|
+
* This is the I/O shell around the pure detector in `src/sources.ts`
|
|
12
|
+
* (`detectPresets`), the same functional-core / imperative-shell split as
|
|
13
|
+
* init.ts ↔ install.ts and skillsCmd.ts ↔ skills.ts. `presets` is NOT on the
|
|
14
|
+
* hook path, so it may exit non-zero on user error and use the TTY
|
|
15
|
+
* (docs/SPECIFICATION.md §9). The detector never throws (a missing/denied dir is
|
|
16
|
+
* recorded as absent), so detect/list always exit 0.
|
|
17
|
+
*/
|
|
18
|
+
export type PresetsSubcommand = "list" | "detect";
|
|
19
|
+
export interface RunPresetsOptions {
|
|
20
|
+
subcommand: PresetsSubcommand;
|
|
21
|
+
/** Test seams. */
|
|
22
|
+
cwd?: string | undefined;
|
|
23
|
+
home?: string | undefined;
|
|
24
|
+
readDir?: ((dir: string) => string[]) | undefined;
|
|
25
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
26
|
+
out?: ((s: string) => void) | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare function runPresets(opts: RunPresetsOptions): number;
|