memhook 0.4.0 → 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 +25 -0
- package/README.md +20 -20
- package/dist/bin/memhook.d.ts +0 -1
- package/dist/bin/memhook.js +36 -2
- 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/ansi.d.ts +0 -1
- package/dist/src/ansi.js +0 -1
- package/dist/src/backup.d.ts +0 -1
- package/dist/src/backup.js +0 -1
- package/dist/src/cache.d.ts +0 -1
- package/dist/src/cache.js +0 -1
- package/dist/src/catalog.d.ts +16 -3
- package/dist/src/catalog.js +50 -5
- package/dist/src/config.d.ts +46 -1
- package/dist/src/config.js +8 -1
- package/dist/src/configFile.d.ts +9 -1
- package/dist/src/configFile.js +0 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +8 -2
- package/dist/src/init.d.ts +0 -1
- package/dist/src/init.js +0 -1
- package/dist/src/install.d.ts +0 -1
- package/dist/src/install.js +0 -1
- package/dist/src/preFilter.d.ts +0 -1
- package/dist/src/preFilter.js +0 -1
- package/dist/src/presetsCmd.d.ts +28 -0
- package/dist/src/presetsCmd.js +94 -0
- package/dist/src/providers/anthropic.d.ts +0 -1
- package/dist/src/providers/anthropic.js +0 -1
- package/dist/src/providers/factory.d.ts +0 -1
- package/dist/src/providers/factory.js +0 -1
- package/dist/src/providers/http.d.ts +0 -1
- package/dist/src/providers/http.js +0 -1
- package/dist/src/providers/ollama.d.ts +0 -1
- package/dist/src/providers/ollama.js +0 -1
- package/dist/src/providers/openai.d.ts +0 -1
- package/dist/src/providers/openai.js +0 -1
- package/dist/src/providers/types.d.ts +0 -1
- package/dist/src/providers/types.js +0 -1
- package/dist/src/router.d.ts +33 -1
- package/dist/src/router.js +141 -45
- package/dist/src/skills.d.ts +0 -1
- package/dist/src/skills.js +0 -1
- package/dist/src/skillsCmd.d.ts +0 -1
- package/dist/src/skillsCmd.js +0 -1
- package/dist/src/sources.d.ts +138 -0
- package/dist/src/sources.js +251 -0
- package/dist/src/tail.d.ts +0 -1
- package/dist/src/tail.js +7 -4
- package/dist/src/version.d.ts +1 -2
- package/dist/src/version.js +1 -2
- package/package.json +1 -1
- package/dist/bin/memhook.d.ts.map +0 -1
- package/dist/bin/memhook.js.map +0 -1
- package/dist/src/ansi.d.ts.map +0 -1
- package/dist/src/ansi.js.map +0 -1
- package/dist/src/backup.d.ts.map +0 -1
- package/dist/src/backup.js.map +0 -1
- package/dist/src/cache.d.ts.map +0 -1
- package/dist/src/cache.js.map +0 -1
- package/dist/src/catalog.d.ts.map +0 -1
- package/dist/src/catalog.js.map +0 -1
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/configFile.d.ts.map +0 -1
- package/dist/src/configFile.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init.d.ts.map +0 -1
- package/dist/src/init.js.map +0 -1
- package/dist/src/install.d.ts.map +0 -1
- package/dist/src/install.js.map +0 -1
- package/dist/src/preFilter.d.ts.map +0 -1
- package/dist/src/preFilter.js.map +0 -1
- package/dist/src/providers/anthropic.d.ts.map +0 -1
- package/dist/src/providers/anthropic.js.map +0 -1
- package/dist/src/providers/factory.d.ts.map +0 -1
- package/dist/src/providers/factory.js.map +0 -1
- package/dist/src/providers/http.d.ts.map +0 -1
- package/dist/src/providers/http.js.map +0 -1
- package/dist/src/providers/ollama.d.ts.map +0 -1
- package/dist/src/providers/ollama.js.map +0 -1
- package/dist/src/providers/openai.d.ts.map +0 -1
- package/dist/src/providers/openai.js.map +0 -1
- package/dist/src/providers/types.d.ts.map +0 -1
- package/dist/src/providers/types.js.map +0 -1
- package/dist/src/router.d.ts.map +0 -1
- package/dist/src/router.js.map +0 -1
- package/dist/src/skills.d.ts.map +0 -1
- package/dist/src/skills.js.map +0 -1
- package/dist/src/skillsCmd.d.ts.map +0 -1
- package/dist/src/skillsCmd.js.map +0 -1
- package/dist/src/tail.d.ts.map +0 -1
- package/dist/src/tail.js.map +0 -1
- package/dist/src/version.d.ts.map +0 -1
- package/dist/src/version.js.map +0 -1
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";
|
|
@@ -23,4 +26,3 @@ export { AnthropicProvider, type AnthropicProviderOptions } from "./providers/an
|
|
|
23
26
|
export { OpenAIProvider } from "./providers/openai.js";
|
|
24
27
|
export { OllamaProvider } from "./providers/ollama.js";
|
|
25
28
|
export type { Provider, ProviderConfig, SelectionRequest, SelectionResponse, UsageBreakdown, } from "./providers/types.js";
|
|
26
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
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";
|
|
15
17
|
export { createProvider } from "./providers/factory.js";
|
|
18
|
+
// ── Internal building blocks (NOT part of the semver-stable surface) ─────────
|
|
19
|
+
// The install/init/skills/tail/ansi re-exports below back the CLI and tests.
|
|
20
|
+
// They are exposed for power users but may change between 0.x minor releases;
|
|
21
|
+
// the stable embedding API is the core above (route, loadConfig, buildCatalog,
|
|
22
|
+
// LocalCache, PreFilter, MEMHOOK_VERSION) plus the provider exports.
|
|
16
23
|
export { addHooks, removeHooks, memhookSubcommand, MEMHOOK_HOOKS, } from "./install.js";
|
|
17
24
|
export { runInit, runUninstall, buildConfigObject, backupPath, } from "./init.js";
|
|
18
25
|
export { COMPANION_SKILLS, SKILL_FILES, isCompanionSkill, diffSkill, planInstall, planUninstall, } from "./skills.js";
|
|
@@ -22,4 +29,3 @@ export { makeAnsi, colorEnabled, visibleWidth } from "./ansi.js";
|
|
|
22
29
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
23
30
|
export { OpenAIProvider } from "./providers/openai.js";
|
|
24
31
|
export { OllamaProvider } from "./providers/ollama.js";
|
|
25
|
-
//# sourceMappingURL=index.js.map
|
package/dist/src/init.d.ts
CHANGED
|
@@ -45,4 +45,3 @@ export declare function buildConfigObject(opts: {
|
|
|
45
45
|
}): Record<string, unknown> | null;
|
|
46
46
|
export declare function runInit(opts: InitOptions, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
47
47
|
export declare function runUninstall(opts: UninstallOptions, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
48
|
-
//# sourceMappingURL=init.d.ts.map
|
package/dist/src/init.js
CHANGED
package/dist/src/install.d.ts
CHANGED
|
@@ -84,4 +84,3 @@ export declare function addHooks(input: unknown, bin?: string): AddResult;
|
|
|
84
84
|
* group list becomes empty — are pruned so no dangling shells are left behind.
|
|
85
85
|
*/
|
|
86
86
|
export declare function removeHooks(input: unknown): RemoveResult;
|
|
87
|
-
//# sourceMappingURL=install.d.ts.map
|
package/dist/src/install.js
CHANGED
package/dist/src/preFilter.d.ts
CHANGED
package/dist/src/preFilter.js
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import { readdirSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { makeAnsi } from "./ansi.js";
|
|
22
|
+
import { HOST_PRESETS, PRESET_NAMES, detectPresets } from "./sources.js";
|
|
23
|
+
function makeIo(env, out) {
|
|
24
|
+
const ansi = makeAnsi({ isTTY: Boolean(process.stdout.isTTY), env });
|
|
25
|
+
return { out: out ?? ((s) => process.stdout.write(s + "\n")), ansi };
|
|
26
|
+
}
|
|
27
|
+
/** Display a source dir + glob with a portable separator (path.join, never "/"). */
|
|
28
|
+
function dirGlob(dir, glob) {
|
|
29
|
+
return join(dir, glob);
|
|
30
|
+
}
|
|
31
|
+
export function runPresets(opts) {
|
|
32
|
+
const env = opts.env ?? process.env;
|
|
33
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
34
|
+
const home = opts.home ?? homedir();
|
|
35
|
+
const readDir = opts.readDir ?? ((dir) => readdirSync(dir));
|
|
36
|
+
const io = makeIo(env, opts.out);
|
|
37
|
+
if (opts.subcommand === "list")
|
|
38
|
+
return runList(io, cwd, home);
|
|
39
|
+
return runDetect(io, cwd, home, readDir);
|
|
40
|
+
}
|
|
41
|
+
function runList(io, cwd, home) {
|
|
42
|
+
const { ansi } = io;
|
|
43
|
+
io.out(ansi.bold("memhook host presets") + ansi.dim(" — built-in source bundles (all experimental)\n"));
|
|
44
|
+
for (const name of PRESET_NAMES) {
|
|
45
|
+
const def = HOST_PRESETS[name];
|
|
46
|
+
if (!def)
|
|
47
|
+
continue;
|
|
48
|
+
io.out(` ${ansi.cyan(name)} ${ansi.dim("(experimental)")}`);
|
|
49
|
+
io.out(` ${ansi.dim(def.summary)}`);
|
|
50
|
+
for (const s of def.sources) {
|
|
51
|
+
const dir = join(s.base === "cwd" ? cwd : home, s.rel);
|
|
52
|
+
io.out(` ${ansi.dim(dirGlob(dir, s.glob))}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
io.out(ansi.dim("\nAll presets are doc-verified but not live-tested → experimental " +
|
|
56
|
+
"until an echo-test.\nEnable in ~/.config/memhook/config.yaml: " +
|
|
57
|
+
"presets: [name, …]\nFind which apply here with `memhook presets detect`."));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
function runDetect(io, cwd, home, readDir) {
|
|
61
|
+
const { ansi } = io;
|
|
62
|
+
const detections = detectPresets(cwd, home, readDir);
|
|
63
|
+
const matched = detections.filter((d) => d.matched);
|
|
64
|
+
io.out(ansi.bold("memhook presets detect") +
|
|
65
|
+
ansi.dim(" — scan this project + home for known host memory\n"));
|
|
66
|
+
for (const d of detections) {
|
|
67
|
+
const total = fileCount(d);
|
|
68
|
+
if (d.matched) {
|
|
69
|
+
io.out(` ${ansi.green("●")} ${ansi.cyan(d.name)} ${ansi.dim(`— ${total} file(s)`)}`);
|
|
70
|
+
for (const dir of d.dirs) {
|
|
71
|
+
if (dir.files.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
io.out(` ${ansi.dim(`${dirGlob(dir.dir, dir.glob)} (${dir.files.length})`)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
io.out(` ${ansi.dim("○")} ${ansi.dim(`${d.name} — no matching files`)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (matched.length === 0) {
|
|
81
|
+
io.out(ansi.dim("\nNo known host memory found under this project or home. Nothing to enable."));
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const names = matched.map((d) => d.name).join(", ");
|
|
85
|
+
io.out("\n" +
|
|
86
|
+
ansi.bold(`Found ${matched.length} host preset(s) with memory.`) +
|
|
87
|
+
ansi.dim(" Enable (experimental) by adding to\n~/.config/memhook/config.yaml:\n"));
|
|
88
|
+
io.out(` ${ansi.cyan(`presets: [${names}]`)}`);
|
|
89
|
+
io.out(ansi.dim("\nThen run `memhook build-catalog` (or restart Claude Code) to catalog them."));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
function fileCount(d) {
|
|
93
|
+
return d.dirs.reduce((n, dir) => n + dir.files.length, 0);
|
|
94
|
+
}
|
package/dist/src/router.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* Never blocks Claude Code.
|
|
20
20
|
*/
|
|
21
21
|
import { type MemhookConfig } from "./config.js";
|
|
22
|
+
import type { HarnessAdapter } from "./adapters/types.js";
|
|
22
23
|
export interface HookInput {
|
|
23
24
|
prompt: string;
|
|
24
25
|
cwd?: string;
|
|
@@ -36,7 +37,19 @@ export interface HookOutput {
|
|
|
36
37
|
*/
|
|
37
38
|
systemMessage?: string;
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Claude Code hook entry point. Unchanged signature and output shape — a thin
|
|
42
|
+
* wrapper that drives the harness-agnostic pipeline through the Claude Code
|
|
43
|
+
* adapter (src/adapters/claudeCode.ts). Byte-identical to the pre-adapter hook.
|
|
44
|
+
*/
|
|
39
45
|
export declare function route(stdinJson: string, env?: NodeJS.ProcessEnv): Promise<HookOutput>;
|
|
46
|
+
/**
|
|
47
|
+
* Generic harness entry point: parse the host's stdin into `{prompt, cwd}` with
|
|
48
|
+
* the adapter, run the selection pipeline, then serialise the result into the
|
|
49
|
+
* host's stdout envelope with the same adapter. The pipeline (`selectMemory`) is
|
|
50
|
+
* identical for every host; only `parseInput` / `formatOutput` differ.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runHarness<T>(adapter: HarnessAdapter<T>, stdinJson: string, env?: NodeJS.ProcessEnv): Promise<T>;
|
|
40
53
|
/**
|
|
41
54
|
* Proactive `/curate` nudge. Returns a one-line `systemMessage` when the memory
|
|
42
55
|
* catalog has grown past a threshold and the cooldown has elapsed, else
|
|
@@ -51,4 +64,23 @@ export declare function route(stdinJson: string, env?: NodeJS.ProcessEnv): Promi
|
|
|
51
64
|
* Exported for direct unit testing.
|
|
52
65
|
*/
|
|
53
66
|
export declare function maybeCurateNudge(config: MemhookConfig, catalogContent: string, now: number): string | undefined;
|
|
54
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Proactive presets nudge. Returns a one-line `systemMessage` when a known
|
|
69
|
+
* host's memory directory exists in this project (or home) but no `presets:`
|
|
70
|
+
* entry routes it yet, else undefined. It makes `memhook presets detect` (the
|
|
71
|
+
* #42 discovery command) self-announcing instead of something the user must know
|
|
72
|
+
* to run.
|
|
73
|
+
*
|
|
74
|
+
* Opt-in is preserved: the nudge only *suggests* the explicit `presets:` config;
|
|
75
|
+
* it never auto-routes anything (every preset is experimental — see
|
|
76
|
+
* `docs/SPECIFICATION.md` §24). Local-only and fully wrapped so any failure
|
|
77
|
+
* yields no nudge (fail-soft is never affected). The cooldown stamp is keyed by
|
|
78
|
+
* cwd because the presets signal is per-project (unlike the catalog-size signal
|
|
79
|
+
* behind the `/curate` nudge), so one project firing must not silence another's.
|
|
80
|
+
* Cost: like the curate nudge, the stamp is written only on a fire, so until one
|
|
81
|
+
* fires the detection runs per prompt — a few `readdir`s on mostly-absent dirs,
|
|
82
|
+
* the same order of I/O `readSelected` already does.
|
|
83
|
+
*
|
|
84
|
+
* Exported for direct unit testing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function maybePresetsNudge(config: MemhookConfig, cwd: string, home: string, now: number): string | undefined;
|
package/dist/src/router.js
CHANGED
|
@@ -20,37 +20,55 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, writeFileSync, openSync, fstatSync, closeSync, appendFileSync, mkdirSync, readdirSync, } from "node:fs";
|
|
22
22
|
import { join, dirname, basename } from "node:path";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
23
25
|
import { LocalCache } from "./cache.js";
|
|
24
26
|
import { loadConfig } from "./config.js";
|
|
25
27
|
import { PreFilter } from "./preFilter.js";
|
|
26
28
|
import { createProvider } from "./providers/factory.js";
|
|
29
|
+
import { activeCustomSources, resolveSources, resolveActivePresetNames, detectPresets, } from "./sources.js";
|
|
30
|
+
import { claudeCodeAdapter } from "./adapters/claudeCode.js";
|
|
27
31
|
const SAFE_BASENAME_RE = /^[A-Za-z0-9._-]+\.md$/;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/** The harness-agnostic empty outcome: inject nothing, no nudge (fail-soft). */
|
|
33
|
+
const EMPTY_RESULT = { additionalContext: "" };
|
|
34
|
+
/**
|
|
35
|
+
* Claude Code hook entry point. Unchanged signature and output shape — a thin
|
|
36
|
+
* wrapper that drives the harness-agnostic pipeline through the Claude Code
|
|
37
|
+
* adapter (src/adapters/claudeCode.ts). Byte-identical to the pre-adapter hook.
|
|
38
|
+
*/
|
|
34
39
|
export async function route(stdinJson, env = process.env) {
|
|
40
|
+
return runHarness(claudeCodeAdapter, stdinJson, env);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generic harness entry point: parse the host's stdin into `{prompt, cwd}` with
|
|
44
|
+
* the adapter, run the selection pipeline, then serialise the result into the
|
|
45
|
+
* host's stdout envelope with the same adapter. The pipeline (`selectMemory`) is
|
|
46
|
+
* identical for every host; only `parseInput` / `formatOutput` differ.
|
|
47
|
+
*/
|
|
48
|
+
export async function runHarness(adapter, stdinJson, env = process.env) {
|
|
49
|
+
const result = await selectMemory(stdinJson, adapter.parseInput, env);
|
|
50
|
+
return adapter.formatOutput(result);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Harness-agnostic selection pipeline. Owns the config gate, prefilter, catalog
|
|
54
|
+
* + API-key checks, the local cache, the provider call, file injection, the
|
|
55
|
+
* JSONL log, and the `/curate` nudge. Every error path returns `EMPTY_RESULT`
|
|
56
|
+
* (fail-soft) — the hook never throws out of here.
|
|
57
|
+
*/
|
|
58
|
+
async function selectMemory(stdinJson, parseInput, env) {
|
|
35
59
|
const config = loadConfig(env);
|
|
36
60
|
ensureDirs(config);
|
|
37
61
|
evictStale(config);
|
|
38
62
|
if (!config.enabled)
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return EMPTY;
|
|
46
|
-
}
|
|
47
|
-
if (!input.prompt || typeof input.prompt !== "string")
|
|
48
|
-
return EMPTY;
|
|
63
|
+
return EMPTY_RESULT;
|
|
64
|
+
const input = parseInput(stdinJson);
|
|
65
|
+
if (!input || !input.prompt || typeof input.prompt !== "string")
|
|
66
|
+
return EMPTY_RESULT;
|
|
49
67
|
const cwd = input.cwd ?? process.cwd();
|
|
50
68
|
const preFilter = new PreFilter(config.preFilter.trivialWordsFile, config.preFilter.defaultWords);
|
|
51
69
|
if (config.preFilter.enabled && preFilter.isTrivial(input.prompt)) {
|
|
52
70
|
logEntry(config, baseLog(input.prompt, "pre_filter_skip"));
|
|
53
|
-
return
|
|
71
|
+
return EMPTY_RESULT;
|
|
54
72
|
}
|
|
55
73
|
// Read the catalog through a single fd: `fstat` gives the cache-key mtime and
|
|
56
74
|
// we read the content from the same handle. Using one open fd — instead of
|
|
@@ -73,14 +91,14 @@ export async function route(stdinJson, env = process.env) {
|
|
|
73
91
|
}
|
|
74
92
|
catch {
|
|
75
93
|
logEntry(config, baseLog(input.prompt, "no_catalog"));
|
|
76
|
-
return
|
|
94
|
+
return EMPTY_RESULT;
|
|
77
95
|
}
|
|
78
96
|
// Provider-aware key gate: local providers (Ollama) need no API key.
|
|
79
97
|
const needsKey = config.provider.type !== "ollama";
|
|
80
98
|
const apiKey = config.provider.apiKeyEnv ? env[config.provider.apiKeyEnv] : undefined;
|
|
81
99
|
if (needsKey && !apiKey) {
|
|
82
100
|
logEntry(config, baseLog(input.prompt, "no_api_key"));
|
|
83
|
-
return
|
|
101
|
+
return EMPTY_RESULT;
|
|
84
102
|
}
|
|
85
103
|
const cache = new LocalCache(config.cache.dir, config.cache.ttlMin, config.cache.evictionDays);
|
|
86
104
|
const cacheKey = config.cache.enabled
|
|
@@ -120,7 +138,7 @@ export async function route(stdinJson, env = process.env) {
|
|
|
120
138
|
}
|
|
121
139
|
catch {
|
|
122
140
|
logEntry(config, baseLog(input.prompt, "provider_init_failed"));
|
|
123
|
-
return
|
|
141
|
+
return EMPTY_RESULT;
|
|
124
142
|
}
|
|
125
143
|
let resp;
|
|
126
144
|
try {
|
|
@@ -133,7 +151,7 @@ export async function route(stdinJson, env = process.env) {
|
|
|
133
151
|
}
|
|
134
152
|
catch {
|
|
135
153
|
logEntry(config, baseLog(input.prompt, "api_no_response"));
|
|
136
|
-
return
|
|
154
|
+
return EMPTY_RESULT;
|
|
137
155
|
}
|
|
138
156
|
latencyMs = resp.latencyMs;
|
|
139
157
|
usage = resp.usage;
|
|
@@ -146,7 +164,7 @@ export async function route(stdinJson, env = process.env) {
|
|
|
146
164
|
cacheCreate: usage.cacheCreateTokens,
|
|
147
165
|
cacheRead: usage.cacheReadTokens,
|
|
148
166
|
});
|
|
149
|
-
return
|
|
167
|
+
return EMPTY_RESULT;
|
|
150
168
|
}
|
|
151
169
|
const extracted = extractJsonArray(resp.rawText);
|
|
152
170
|
if (extracted === null) {
|
|
@@ -158,7 +176,7 @@ export async function route(stdinJson, env = process.env) {
|
|
|
158
176
|
cacheCreate: usage.cacheCreateTokens,
|
|
159
177
|
cacheRead: usage.cacheReadTokens,
|
|
160
178
|
});
|
|
161
|
-
return
|
|
179
|
+
return EMPTY_RESULT;
|
|
162
180
|
}
|
|
163
181
|
selectedJson = JSON.stringify(extracted);
|
|
164
182
|
if (config.cache.enabled && selectedJson !== "[]") {
|
|
@@ -192,19 +210,25 @@ export async function route(stdinJson, env = process.env) {
|
|
|
192
210
|
additionalSizeTokensEst: Math.floor(additional.length / 4),
|
|
193
211
|
status,
|
|
194
212
|
});
|
|
195
|
-
const
|
|
196
|
-
hookSpecificOutput: {
|
|
197
|
-
hookEventName: "UserPromptSubmit",
|
|
198
|
-
additionalContext: additional,
|
|
199
|
-
},
|
|
200
|
-
};
|
|
213
|
+
const result = { additionalContext: additional };
|
|
201
214
|
// Proactive `/curate` nudge — best-effort, local-only, never affects the
|
|
202
215
|
// additionalContext contract or fail-soft. `catalogContent` is already in hand
|
|
203
|
-
// (read above), so the catalog-size signal is free.
|
|
216
|
+
// (read above), so the catalog-size signal is free. The adapter serialises it
|
|
217
|
+
// into the host's notice channel (Claude Code: `systemMessage`); a host with
|
|
218
|
+
// no equivalent simply drops it.
|
|
204
219
|
const nudge = maybeCurateNudge(config, catalogContent, Date.now());
|
|
205
|
-
if (nudge)
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
if (nudge) {
|
|
221
|
+
result.systemMessage = nudge;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Only one notice channel (`systemMessage`) exists, so the presets nudge is
|
|
225
|
+
// a fallback: it fires only on a turn the curate nudge did not. Each has its
|
|
226
|
+
// own long cooldown, so the collision is rare and never produces two notices.
|
|
227
|
+
const pnudge = maybePresetsNudge(config, cwd, homedir(), Date.now());
|
|
228
|
+
if (pnudge)
|
|
229
|
+
result.systemMessage = pnudge;
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
208
232
|
}
|
|
209
233
|
function buildSystemPrompt(catalog) {
|
|
210
234
|
return `Tu es un sélecteur de mémoire pour Claude Code. Tu identifies les feedbacks et règles pertinents pour le prompt utilisateur. Tu réponds UNIQUEMENT avec un JSON array de basenames .md, sans explication, sans markdown code fence.
|
|
@@ -246,33 +270,53 @@ function parseBasenames(raw) {
|
|
|
246
270
|
}
|
|
247
271
|
function extractJsonArray(text) {
|
|
248
272
|
const flat = text.replace(/\n/g, " ");
|
|
249
|
-
const
|
|
250
|
-
if (!
|
|
273
|
+
const matches = flat.match(/\[[^\]]*\]/g);
|
|
274
|
+
if (!matches)
|
|
251
275
|
return null;
|
|
252
|
-
|
|
253
|
-
|
|
276
|
+
// A compliant response is a single bare array, but a model may wrap it in
|
|
277
|
+
// prose containing a decoy `[...]`. Scan every bracketed candidate and prefer
|
|
278
|
+
// the LAST one that yields usable string basenames; keep an empty array only
|
|
279
|
+
// as a fallback when no non-empty array is found, else null.
|
|
280
|
+
let result = null;
|
|
281
|
+
for (const candidate of matches) {
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = JSON.parse(candidate);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
254
289
|
if (!Array.isArray(parsed))
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return null;
|
|
290
|
+
continue;
|
|
291
|
+
const strings = parsed.filter((x) => typeof x === "string");
|
|
292
|
+
if (strings.length > 0 || result === null)
|
|
293
|
+
result = strings;
|
|
260
294
|
}
|
|
295
|
+
return result;
|
|
261
296
|
}
|
|
262
297
|
function readSelected(basenames, cwd, config) {
|
|
263
|
-
// Build search dirs: ~/.claude/projects/*/memory + global rules + cwd rules
|
|
298
|
+
// Build search dirs: ~/.claude/projects/*/memory + global rules + cwd rules +
|
|
299
|
+
// any user-declared custom sources (host-autoloaded ones only when resurfacing,
|
|
300
|
+
// mirroring the catalog so the router never finds what the catalog omitted).
|
|
264
301
|
const projectDirs = listProjectsMemoryDirs(config.searchDirs[0]);
|
|
265
302
|
const rulesDir = config.searchDirs[1];
|
|
266
303
|
const cwdRulesDir = join(cwd, ".claude", "rules");
|
|
267
|
-
const
|
|
304
|
+
const allSources = resolveSources(config.customSources, config.presets, cwd, homedir(), readdirSync);
|
|
305
|
+
const customDirs = activeCustomSources(allSources, config.resurfaceHostLoaded).map((s) => s.dir);
|
|
306
|
+
const dirs = [...projectDirs, rulesDir, cwdRulesDir, ...customDirs].filter((d) => typeof d === "string" && d.length > 0);
|
|
268
307
|
let additional = "";
|
|
269
308
|
let injected = 0;
|
|
270
309
|
const seen = [];
|
|
310
|
+
const seenNames = new Set();
|
|
271
311
|
for (const name of basenames) {
|
|
272
312
|
if (injected >= config.selection.maxFiles)
|
|
273
313
|
break;
|
|
274
314
|
if (!SAFE_BASENAME_RE.test(name))
|
|
275
315
|
continue;
|
|
316
|
+
// De-dup: a basename the model repeats is injected once and uses one slot.
|
|
317
|
+
if (seenNames.has(name))
|
|
318
|
+
continue;
|
|
319
|
+
seenNames.add(name);
|
|
276
320
|
seen.push(name);
|
|
277
321
|
for (const dir of dirs) {
|
|
278
322
|
const file = join(dir, name);
|
|
@@ -366,6 +410,59 @@ export function maybeCurateNudge(config, catalogContent, now) {
|
|
|
366
410
|
return undefined; // a nudge must never break fail-soft
|
|
367
411
|
}
|
|
368
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Proactive presets nudge. Returns a one-line `systemMessage` when a known
|
|
415
|
+
* host's memory directory exists in this project (or home) but no `presets:`
|
|
416
|
+
* entry routes it yet, else undefined. It makes `memhook presets detect` (the
|
|
417
|
+
* #42 discovery command) self-announcing instead of something the user must know
|
|
418
|
+
* to run.
|
|
419
|
+
*
|
|
420
|
+
* Opt-in is preserved: the nudge only *suggests* the explicit `presets:` config;
|
|
421
|
+
* it never auto-routes anything (every preset is experimental — see
|
|
422
|
+
* `docs/SPECIFICATION.md` §24). Local-only and fully wrapped so any failure
|
|
423
|
+
* yields no nudge (fail-soft is never affected). The cooldown stamp is keyed by
|
|
424
|
+
* cwd because the presets signal is per-project (unlike the catalog-size signal
|
|
425
|
+
* behind the `/curate` nudge), so one project firing must not silence another's.
|
|
426
|
+
* Cost: like the curate nudge, the stamp is written only on a fire, so until one
|
|
427
|
+
* fires the detection runs per prompt — a few `readdir`s on mostly-absent dirs,
|
|
428
|
+
* the same order of I/O `readSelected` already does.
|
|
429
|
+
*
|
|
430
|
+
* Exported for direct unit testing.
|
|
431
|
+
*/
|
|
432
|
+
export function maybePresetsNudge(config, cwd, home, now) {
|
|
433
|
+
try {
|
|
434
|
+
if (!config.presetsNudge.enabled)
|
|
435
|
+
return undefined;
|
|
436
|
+
// Per-project stamp: a global stamp would let the first project hit in a
|
|
437
|
+
// window silence every other project's (per-cwd) presets signal.
|
|
438
|
+
const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
439
|
+
const stampFile = join(config.cache.dir, `.presets-nudge-${cwdHash}`);
|
|
440
|
+
const last = readNudgeStamp(stampFile);
|
|
441
|
+
if (last !== null && now - last < config.presetsNudge.cooldownDays * 86_400_000) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
// Suggest a preset only when it has memory on disk that is NOT already routed
|
|
445
|
+
// — neither by an effective preset name (named entries AND `presets: [auto]`
|
|
446
|
+
// expansion, so an `auto` user is never nudged) nor by a hand-written
|
|
447
|
+
// `customSources` dir pointing at the same place (`presets:` is sugar over
|
|
448
|
+
// `customSources`, D31/D32). `enabled` keys on the EXPANDED names (not the raw
|
|
449
|
+
// config) so it suppresses regardless of the `resurfaceHostLoaded` gate, which
|
|
450
|
+
// would otherwise drop a hostAutoLoaded preset's dirs from `routedDirs`.
|
|
451
|
+
const enabled = new Set(resolveActivePresetNames(config.presets, cwd, home, readdirSync));
|
|
452
|
+
const routedDirs = new Set(activeCustomSources(resolveSources(config.customSources, config.presets, cwd, home, readdirSync), config.resurfaceHostLoaded).map((s) => s.dir));
|
|
453
|
+
const found = detectPresets(cwd, home, readdirSync)
|
|
454
|
+
.filter((d) => d.matched && !enabled.has(d.name))
|
|
455
|
+
.filter((d) => d.dirs.some((dir) => dir.files.length > 0 && !routedDirs.has(dir.dir)))
|
|
456
|
+
.map((d) => d.name);
|
|
457
|
+
if (found.length === 0)
|
|
458
|
+
return undefined;
|
|
459
|
+
writeNudgeStamp(stampFile, now);
|
|
460
|
+
return `🔌 memhook: found ${found.join(", ")} memory not yet routed by memhook. Run \`memhook presets detect\` to enable it.`;
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return undefined; // a nudge must never break fail-soft
|
|
464
|
+
}
|
|
465
|
+
}
|
|
369
466
|
/** Count top-level `*.md` memory files (excludes MEMORY.md and the journal/ subdir). */
|
|
370
467
|
function countMemoryFiles(projectsRoot) {
|
|
371
468
|
let total = 0;
|
|
@@ -444,4 +541,3 @@ function logEntry(config, entry) {
|
|
|
444
541
|
function nowIso() {
|
|
445
542
|
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
446
543
|
}
|
|
447
|
-
//# sourceMappingURL=router.js.map
|
package/dist/src/skills.d.ts
CHANGED
|
@@ -65,4 +65,3 @@ export interface SkillUninstallPlan {
|
|
|
65
65
|
}
|
|
66
66
|
/** Plan one skill uninstall: remove only the files memhook ships, if present. */
|
|
67
67
|
export declare function planUninstall(name: CompanionSkill, source: SkillSources, installed: InstalledFiles): SkillUninstallPlan;
|
|
68
|
-
//# sourceMappingURL=skills.d.ts.map
|
package/dist/src/skills.js
CHANGED
package/dist/src/skillsCmd.d.ts
CHANGED
package/dist/src/skillsCmd.js
CHANGED