@cachly-dev/mcp-server 0.10.129 → 0.10.131

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 (223) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1 -7
  3. package/dist/packages/telegram-notify/client.js +2 -75
  4. package/dist/src/ambient-cli.js +7 -253
  5. package/dist/src/ambient-deps.js +1 -55
  6. package/dist/src/ambient-hooks.js +5 -278
  7. package/dist/src/ambient-ledger.js +6 -95
  8. package/dist/src/ambient-memory.js +1 -67
  9. package/dist/src/ambient-recall.js +1 -221
  10. package/dist/src/aussetzer.js +1 -225
  11. package/dist/src/auth.js +9 -140
  12. package/dist/src/autor-abzeichen.js +1 -56
  13. package/dist/src/autor.js +1 -136
  14. package/dist/src/bauabdruck.js +1 -93
  15. package/dist/src/bedeutung.js +2 -323
  16. package/dist/src/bench/cachly-bench.js +4 -249
  17. package/dist/src/bench/external-corpus.js +4 -135
  18. package/dist/src/bench/fixtures.js +1 -225
  19. package/dist/src/brain-export.js +16 -119
  20. package/dist/src/brain-watch-hook.js +8 -86
  21. package/dist/src/cachly-url.js +1 -21
  22. package/dist/src/ckg.js +1 -108
  23. package/dist/src/cls-hook.js +8 -94
  24. package/dist/src/confidence.js +1 -50
  25. package/dist/src/credentials.js +1 -85
  26. package/dist/src/deutsch.js +1 -223
  27. package/dist/src/doctor.js +2 -248
  28. package/dist/src/editor.js +1 -63
  29. package/dist/src/eingaenge-nachruesten.js +1 -290
  30. package/dist/src/eingaenge.js +2 -260
  31. package/dist/src/embeddings.js +13 -223
  32. package/dist/src/feldreparatur.js +3 -113
  33. package/dist/src/first-contact.js +4 -108
  34. package/dist/src/first-recall-proof.js +4 -41
  35. package/dist/src/funnel-milestones.js +2 -48
  36. package/dist/src/geliefert.js +1 -114
  37. package/dist/src/handlers/advanced.js +38 -537
  38. package/dist/src/handlers/brain.js +69 -4397
  39. package/dist/src/handlers/cache.js +59 -638
  40. package/dist/src/handlers/context.js +17 -182
  41. package/dist/src/handlers/fedbrain.js +40 -1436
  42. package/dist/src/handlers/instances.js +16 -201
  43. package/dist/src/handlers/roadmap.js +13 -175
  44. package/dist/src/handlers/share.js +19 -530
  45. package/dist/src/handlers/syndicate.js +38 -904
  46. package/dist/src/handlers/team.js +89 -1865
  47. package/dist/src/handlers/viz.js +3 -148
  48. package/dist/src/index.js +321 -4281
  49. package/dist/src/interferenz.js +1 -141
  50. package/dist/src/kostprobe.js +7 -125
  51. package/dist/src/lesson-preview.js +2 -133
  52. package/dist/src/namespace.js +1 -128
  53. package/dist/src/notifier.js +1 -6
  54. package/dist/src/rangfolge-stellschrauben.js +1 -0
  55. package/dist/src/rangfolge.js +1 -249
  56. package/dist/src/raumkorrektur.js +1 -197
  57. package/dist/src/recall-tiefe.js +1 -42
  58. package/dist/src/rerank.js +1 -154
  59. package/dist/src/resonanz.js +1 -140
  60. package/dist/src/search.js +1 -2104
  61. package/dist/src/seltenheitsbestand.js +1 -92
  62. package/dist/src/speicherlage.js +1 -84
  63. package/dist/src/spuren.js +1 -150
  64. package/dist/src/starter-corpus.js +1 -165
  65. package/dist/src/telemetry-types.js +0 -13
  66. package/dist/src/tools.js +10 -2465
  67. package/dist/src/toolspecs.js +1 -121
  68. package/dist/src/upgrade-nudge.js +1 -29
  69. package/dist/src/utils.js +1 -89
  70. package/dist/src/versuch.js +1 -264
  71. package/dist/src/vorspann.js +1 -146
  72. package/dist/src/wartung/vektoren-verdichten.js +1 -200
  73. package/dist/src/werkzeug-nutzung.js +1 -72
  74. package/dist/src/wertbeitrag.js +1 -203
  75. package/dist/src/wirkungsspur.js +1 -0
  76. package/glama.json +1 -1
  77. package/package.json +8 -7
  78. package/server.json +3 -3
  79. package/dist/index.d.ts +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/packages/telegram-notify/client.d.ts +0 -19
  82. package/dist/packages/telegram-notify/client.js.map +0 -1
  83. package/dist/src/ambient-cli.d.ts +0 -101
  84. package/dist/src/ambient-cli.js.map +0 -1
  85. package/dist/src/ambient-deps.d.ts +0 -26
  86. package/dist/src/ambient-deps.js.map +0 -1
  87. package/dist/src/ambient-hooks.d.ts +0 -137
  88. package/dist/src/ambient-hooks.js.map +0 -1
  89. package/dist/src/ambient-ledger.d.ts +0 -19
  90. package/dist/src/ambient-ledger.js.map +0 -1
  91. package/dist/src/ambient-memory.d.ts +0 -12
  92. package/dist/src/ambient-memory.js.map +0 -1
  93. package/dist/src/ambient-recall.d.ts +0 -127
  94. package/dist/src/ambient-recall.js.map +0 -1
  95. package/dist/src/aussetzer.d.ts +0 -156
  96. package/dist/src/aussetzer.js.map +0 -1
  97. package/dist/src/auth.d.ts +0 -43
  98. package/dist/src/auth.js.map +0 -1
  99. package/dist/src/autor-abzeichen.d.ts +0 -41
  100. package/dist/src/autor-abzeichen.js.map +0 -1
  101. package/dist/src/autor.d.ts +0 -101
  102. package/dist/src/autor.js.map +0 -1
  103. package/dist/src/bauabdruck-daten.generiert.json +0 -5
  104. package/dist/src/bauabdruck.d.ts +0 -38
  105. package/dist/src/bauabdruck.js.map +0 -1
  106. package/dist/src/bedeutung.d.ts +0 -168
  107. package/dist/src/bedeutung.js.map +0 -1
  108. package/dist/src/bench/cachly-bench.d.ts +0 -61
  109. package/dist/src/bench/cachly-bench.js.map +0 -1
  110. package/dist/src/bench/external-corpus.d.ts +0 -44
  111. package/dist/src/bench/external-corpus.js.map +0 -1
  112. package/dist/src/bench/fixtures.d.ts +0 -37
  113. package/dist/src/bench/fixtures.js.map +0 -1
  114. package/dist/src/brain-export.d.ts +0 -64
  115. package/dist/src/brain-export.js.map +0 -1
  116. package/dist/src/brain-watch-hook.d.ts +0 -21
  117. package/dist/src/brain-watch-hook.js.map +0 -1
  118. package/dist/src/cachly-url.d.ts +0 -68
  119. package/dist/src/cachly-url.js.map +0 -1
  120. package/dist/src/ckg.d.ts +0 -54
  121. package/dist/src/ckg.js.map +0 -1
  122. package/dist/src/cls-hook.d.ts +0 -15
  123. package/dist/src/cls-hook.js.map +0 -1
  124. package/dist/src/confidence.d.ts +0 -19
  125. package/dist/src/confidence.js.map +0 -1
  126. package/dist/src/credentials.d.ts +0 -37
  127. package/dist/src/credentials.js.map +0 -1
  128. package/dist/src/deutsch.d.ts +0 -98
  129. package/dist/src/deutsch.js.map +0 -1
  130. package/dist/src/doctor.d.ts +0 -47
  131. package/dist/src/doctor.js.map +0 -1
  132. package/dist/src/editor.d.ts +0 -9
  133. package/dist/src/editor.js.map +0 -1
  134. package/dist/src/eingaenge-nachruesten.d.ts +0 -41
  135. package/dist/src/eingaenge-nachruesten.js.map +0 -1
  136. package/dist/src/eingaenge.d.ts +0 -159
  137. package/dist/src/eingaenge.js.map +0 -1
  138. package/dist/src/embeddings.d.ts +0 -48
  139. package/dist/src/embeddings.js.map +0 -1
  140. package/dist/src/feldreparatur.d.ts +0 -58
  141. package/dist/src/feldreparatur.js.map +0 -1
  142. package/dist/src/first-contact.d.ts +0 -56
  143. package/dist/src/first-contact.js.map +0 -1
  144. package/dist/src/first-recall-proof.d.ts +0 -19
  145. package/dist/src/first-recall-proof.js.map +0 -1
  146. package/dist/src/funnel-milestones.d.ts +0 -5
  147. package/dist/src/funnel-milestones.js.map +0 -1
  148. package/dist/src/geliefert.d.ts +0 -63
  149. package/dist/src/geliefert.js.map +0 -1
  150. package/dist/src/handlers/advanced.d.ts +0 -6
  151. package/dist/src/handlers/advanced.js.map +0 -1
  152. package/dist/src/handlers/brain.d.ts +0 -41
  153. package/dist/src/handlers/brain.js.map +0 -1
  154. package/dist/src/handlers/cache.d.ts +0 -6
  155. package/dist/src/handlers/cache.js.map +0 -1
  156. package/dist/src/handlers/context.d.ts +0 -6
  157. package/dist/src/handlers/context.js.map +0 -1
  158. package/dist/src/handlers/fedbrain.d.ts +0 -18
  159. package/dist/src/handlers/fedbrain.js.map +0 -1
  160. package/dist/src/handlers/instances.d.ts +0 -33
  161. package/dist/src/handlers/instances.js.map +0 -1
  162. package/dist/src/handlers/roadmap.d.ts +0 -6
  163. package/dist/src/handlers/roadmap.js.map +0 -1
  164. package/dist/src/handlers/share.d.ts +0 -8
  165. package/dist/src/handlers/share.js.map +0 -1
  166. package/dist/src/handlers/syndicate.d.ts +0 -6
  167. package/dist/src/handlers/syndicate.js.map +0 -1
  168. package/dist/src/handlers/team.d.ts +0 -45
  169. package/dist/src/handlers/team.js.map +0 -1
  170. package/dist/src/handlers/viz.d.ts +0 -6
  171. package/dist/src/handlers/viz.js.map +0 -1
  172. package/dist/src/index.d.ts +0 -93
  173. package/dist/src/index.js.map +0 -1
  174. package/dist/src/interferenz.d.ts +0 -111
  175. package/dist/src/interferenz.js.map +0 -1
  176. package/dist/src/kostprobe.d.ts +0 -25
  177. package/dist/src/kostprobe.js.map +0 -1
  178. package/dist/src/lesson-preview.d.ts +0 -81
  179. package/dist/src/lesson-preview.js.map +0 -1
  180. package/dist/src/namespace.d.ts +0 -20
  181. package/dist/src/namespace.js.map +0 -1
  182. package/dist/src/notifier.d.ts +0 -5
  183. package/dist/src/notifier.js.map +0 -1
  184. package/dist/src/rangfolge.d.ts +0 -205
  185. package/dist/src/rangfolge.js.map +0 -1
  186. package/dist/src/raumkorrektur.d.ts +0 -100
  187. package/dist/src/raumkorrektur.js.map +0 -1
  188. package/dist/src/recall-tiefe.d.ts +0 -48
  189. package/dist/src/recall-tiefe.js.map +0 -1
  190. package/dist/src/rerank.d.ts +0 -48
  191. package/dist/src/rerank.js.map +0 -1
  192. package/dist/src/resonanz.d.ts +0 -85
  193. package/dist/src/resonanz.js.map +0 -1
  194. package/dist/src/search.d.ts +0 -153
  195. package/dist/src/search.js.map +0 -1
  196. package/dist/src/seltenheitsbestand.d.ts +0 -53
  197. package/dist/src/seltenheitsbestand.js.map +0 -1
  198. package/dist/src/speicherlage.d.ts +0 -51
  199. package/dist/src/speicherlage.js.map +0 -1
  200. package/dist/src/spuren.d.ts +0 -97
  201. package/dist/src/spuren.js.map +0 -1
  202. package/dist/src/starter-corpus.d.ts +0 -28
  203. package/dist/src/starter-corpus.js.map +0 -1
  204. package/dist/src/telemetry-types.d.ts +0 -87
  205. package/dist/src/telemetry-types.js.map +0 -1
  206. package/dist/src/tools.d.ts +0 -2933
  207. package/dist/src/tools.js.map +0 -1
  208. package/dist/src/toolspecs.d.ts +0 -43
  209. package/dist/src/toolspecs.js.map +0 -1
  210. package/dist/src/upgrade-nudge.d.ts +0 -15
  211. package/dist/src/upgrade-nudge.js.map +0 -1
  212. package/dist/src/utils.d.ts +0 -34
  213. package/dist/src/utils.js.map +0 -1
  214. package/dist/src/versuch.d.ts +0 -187
  215. package/dist/src/versuch.js.map +0 -1
  216. package/dist/src/vorspann.d.ts +0 -73
  217. package/dist/src/vorspann.js.map +0 -1
  218. package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
  219. package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
  220. package/dist/src/werkzeug-nutzung.d.ts +0 -62
  221. package/dist/src/werkzeug-nutzung.js.map +0 -1
  222. package/dist/src/wertbeitrag.d.ts +0 -148
  223. package/dist/src/wertbeitrag.js.map +0 -1
@@ -1,278 +1,5 @@
1
- // Ambient Recall (Phase 4) — Claude Code hook-script generators.
2
- //
3
- // Builds the SessionStart + UserPromptSubmit hook scripts (and the
4
- // .claude/settings.json fragment that wires them) so recall is PUSHED into
5
- // context automatically, without the agent having to remember to call it
6
- // (roadmap §6.1). This is the packaging that turns cachly's existing recall
7
- // into "memory that is just there".
8
- //
9
- // Design mirrors cls-hook.ts:
10
- // • Pure string builders — exhaustively unit-tested.
11
- // • No untrusted input is interpolated into the script; the hook payload
12
- // (which contains the user's prompt) is piped to the CLI on stdin, never
13
- // spliced into shell/JS source.
14
- // • Versioned so installers can upgrade old hooks in place.
15
- // • Graceful: a crashing hook must NEVER block the agent — every path exits 0
16
- // with no output, so Claude Code simply proceeds without the extra context
17
- // (§6.3 guardrail 5).
18
- import { existsSync } from 'node:fs';
19
- import { readFile, writeFile, chmod, mkdir } from 'node:fs/promises';
20
- import { resolve } from 'node:path';
21
- /**
22
- * Bumped whenever a hook script changes so installers can upgrade old hooks.
23
- * v3: hooks became Node scripts (.mjs) invoked as `node "<path>"` — the
24
- * cross-platform shape from the Claude Code hooks guide. The v1/v2 POSIX shell
25
- * scripts silently never ran on native Windows (no /bin/sh); Node is already a
26
- * hard requirement of this package, so the hook now runs identically on
27
- * Windows, macOS and Linux.
28
- * v4 (GROW-015): the script no longer embeds the caller's key as a string
29
- * literal. It resolves CACHLY_JWT at run time from the environment instead,
30
- * so a `.claude/hooks/` script that gets committed to a repo never leaks a
31
- * secret. Bumping the version replaces old v1-v3 scripts (which DID embed
32
- * the key) the next time setup/autopilot runs.
33
- */
34
- export const AMBIENT_HOOK_VERSION = 'v4';
35
- /**
36
- * The CLI subcommand the hooks pipe their payload to. It reads the hook JSON on
37
- * stdin, runs smart_recall through the relevance gate (ambient-recall.ts), and
38
- * prints the `hookSpecificOutput` JSON Claude Code injects as additionalContext.
39
- * The CLI self-limits its latency budget (§6.3 guardrail 4). See ambient-cli.ts.
40
- */
41
- export const AMBIENT_CLI_SUBCOMMAND = 'ambient-recall';
42
- const DEFAULT_CLI = `npx @cachly-dev/mcp-server@latest ${AMBIENT_CLI_SUBCOMMAND}`;
43
- /** Escape a value for embedding inside a single-quoted JS string literal. */
44
- function jsString(s) {
45
- return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
46
- }
47
- function buildHook(event, opts) {
48
- const cli = opts.cliCommand ?? DEFAULT_CLI;
49
- return [
50
- `#!/usr/bin/env node`,
51
- `// cachly Ambient Recall — ${event} ${AMBIENT_HOOK_VERSION}`,
52
- `// Pushes relevant memory into context automatically. Cross-platform Node hook`,
53
- `// (no shell script — runs identically on Windows/macOS/Linux). Never blocks`,
54
- `// the agent: every failure path exits 0 with no output (graceful degrade).`,
55
- `import { spawn } from 'node:child_process';`,
56
- `process.env.CACHLY_BRAIN_INSTANCE_ID = '${jsString(opts.instanceId)}';`,
57
- // GROW-015: never embed the caller's key as a string literal here — read
58
- // it at run time instead, so a committed .claude/hooks/ script can never
59
- // leak a secret. Only the key's origin changed: same events, same
60
- // output, same 3s budget, errors still swallowed.
61
- `if (!process.env.CACHLY_JWT && process.env.CACHLY_API_KEY) process.env.CACHLY_JWT = process.env.CACHLY_API_KEY;`,
62
- `process.env.CACHLY_HOOK_EVENT = '${jsString(event)}';`,
63
- `try {`,
64
- // Claude Code delivers the hook payload as JSON on stdin; stdio 'inherit'
65
- // hands it verbatim to the CLI (no re-parsing of prompt content), and the
66
- // CLI's hookSpecificOutput JSON flows straight back to Claude Code on
67
- // stdout. shell:true resolves npx/npx.cmd on every platform; stderr is
68
- // discarded so a noisy npm can never corrupt the hook protocol.
69
- ` const child = spawn('${jsString(cli)}', { shell: true, stdio: ['inherit', 'inherit', 'ignore'] });`,
70
- ` child.on('error', () => process.exit(0));`,
71
- ` child.on('close', () => process.exit(0));`,
72
- `} catch {`,
73
- ` process.exit(0);`,
74
- `}`,
75
- ].join('\n');
76
- }
77
- /**
78
- * SessionStart hook: emits the session briefing as additionalContext at the
79
- * start of every session — the automatic replacement for "the agent should call
80
- * session_start".
81
- */
82
- export function buildSessionStartHook(opts) {
83
- return buildHook('SessionStart', opts);
84
- }
85
- /**
86
- * UserPromptSubmit hook — the core of Ambient Recall. Runs before every user
87
- * message, recalls on the prompt through the relevance gate, and pushes gated
88
- * hits into context. This is the most-forgotten call, made automatic.
89
- */
90
- export function buildUserPromptSubmitHook(opts) {
91
- return buildHook('UserPromptSubmit', opts);
92
- }
93
- /**
94
- * PreToolUse hook (Ausbau) — file-open briefing. Fires before Edit/Write-class
95
- * tools (settings matcher `Edit|Write|MultiEdit`); the CLI recalls file-scoped
96
- * lessons and injects them as additionalContext — the automatic "prüf mal, ob's
97
- * zu dieser Datei Lessons gibt" (roadmap §6.1).
98
- */
99
- export function buildPreToolUseHook(opts) {
100
- return buildHook('PreToolUse', opts);
101
- }
102
- /**
103
- * Stop hook (Ausbau) — auto-learn. After a turn whose final message carries a
104
- * clear fix signal, the CLI feeds one observation to auto_learn_session — the
105
- * automatic replacement for the forgotten `learn_from_attempts` call.
106
- */
107
- export function buildStopHook(opts) {
108
- return buildHook('Stop', opts);
109
- }
110
- // Per-event latency budgets (seconds). The CLI self-limits recall to 3s; these
111
- // are the outer safety net so a wedged npx can never stall a turn for long.
112
- const EVENT_TIMEOUTS = {
113
- SessionStart: 30,
114
- UserPromptSubmit: 10,
115
- PreToolUse: 10,
116
- Stop: 60, // auto-learn may do a real write; Stop is not latency-critical
117
- };
118
- /** Matcher for the PreToolUse briefing: only file-mutating tools. */
119
- export const PRE_TOOL_USE_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
120
- /**
121
- * Build the `.claude/settings.json` `hooks` fragment that wires the scripts.
122
- * The caller merges this into the user's existing settings (never overwrites).
123
- * Shape matches Claude Code's hooks config: an array of matcher groups, each
124
- * with a list of `{ type: "command", command, timeout }` entries.
125
- */
126
- export function buildAmbientSettingsHooks(paths) {
127
- // v3: the command is `node "<script>"` — a plain string that both /bin/sh
128
- // (macOS/Linux, Windows+Git-Bash) and PowerShell (native Windows fallback)
129
- // execute identically, so one settings shape covers every platform.
130
- const entry = (event, scriptPath, matcher) => ({
131
- ...(matcher ? { matcher } : {}),
132
- hooks: [{ type: 'command', command: `node "${scriptPath}"`, timeout: EVENT_TIMEOUTS[event] }],
133
- });
134
- const frag = {
135
- SessionStart: [entry('SessionStart', paths.sessionStart)],
136
- UserPromptSubmit: [entry('UserPromptSubmit', paths.userPromptSubmit)],
137
- };
138
- if (paths.preToolUse)
139
- frag.PreToolUse = [entry('PreToolUse', paths.preToolUse, PRE_TOOL_USE_MATCHER)];
140
- if (paths.stop)
141
- frag.Stop = [entry('Stop', paths.stop)];
142
- return frag;
143
- }
144
- // ── Installer ────────────────────────────────────────────────────────────────
145
- // Writes the two hook scripts into `.claude/hooks/` and merges the settings
146
- // fragment into `.claude/settings.json`. Idempotent and non-destructive:
147
- // • re-running upgrades the scripts in place (version marker) and never
148
- // duplicates a settings entry that already points at our script,
149
- // • foreign hooks in the user's settings are preserved (we append, never
150
- // overwrite the arrays).
151
- // Never throws — returns a status the caller can log.
152
- const HOOK_DIR = '.claude/hooks';
153
- const SETTINGS_FILE = '.claude/settings.json';
154
- /** Script filename per event — shared marker `cachly-ambient-` drives upgrades. */
155
- const SCRIPT_NAMES = {
156
- SessionStart: 'cachly-ambient-session-start.mjs',
157
- UserPromptSubmit: 'cachly-ambient-prompt-submit.mjs',
158
- PreToolUse: 'cachly-ambient-pre-tool.mjs',
159
- Stop: 'cachly-ambient-stop.mjs',
160
- };
161
- const AMBIENT_SCRIPT_MARKER = '.claude/hooks/cachly-ambient-';
162
- /** True when this matcher group only wires cachly-ambient scripts. */
163
- function isAmbientGroup(g, currentPaths) {
164
- const hooks = g.hooks ?? [];
165
- return (hooks.length > 0 &&
166
- hooks.every((h) => {
167
- const cmd = h.command ?? '';
168
- // Matches v1/v2 entries (bare script path), v3 entries (`node "<path>"`),
169
- // and marker-less test/custom paths passed as the current fragment.
170
- return (cmd.includes(AMBIENT_SCRIPT_MARKER) ||
171
- currentPaths.has(cmd) ||
172
- [...currentPaths].some((p) => cmd.includes(`"${p}"`)));
173
- }));
174
- }
175
- /**
176
- * Merge our hook entries into an existing settings object without disturbing
177
- * anything else. Idempotent AND upgrade-safe: existing cachly-ambient groups
178
- * (from any prior version/paths) are replaced by the current fragment rather
179
- * than accumulated; foreign groups are always preserved. Pure — unit-tested.
180
- */
181
- export function mergeAmbientSettings(existing, paths) {
182
- const next = { ...existing, hooks: { ...(existing.hooks ?? {}) } };
183
- const hooks = next.hooks;
184
- const fragment = buildAmbientSettingsHooks(paths);
185
- const currentPaths = new Set([paths.sessionStart, paths.userPromptSubmit, paths.preToolUse, paths.stop].filter((p) => !!p));
186
- for (const [event, ourGroups] of Object.entries(fragment)) {
187
- const foreign = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !isAmbientGroup(g, currentPaths));
188
- hooks[event] = [...foreign, ...ourGroups];
189
- }
190
- const changed = JSON.stringify(next) !== JSON.stringify(existing);
191
- return { settings: next, changed };
192
- }
193
- /**
194
- * Install/upgrade the Ambient Recall hooks in `projectDir`. Best-effort:
195
- * a filesystem error surfaces as a thrown error only for the top-level caller,
196
- * which wraps it in try/catch (the git-hook feature is non-critical).
197
- */
198
- export async function installAmbientHooks(projectDir, instanceId, apiKey, options = {}) {
199
- const hookDir = resolve(projectDir, HOOK_DIR);
200
- await mkdir(hookDir, { recursive: true });
201
- const opts = { instanceId, apiKey };
202
- const events = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'Stop'];
203
- const scriptFor = {
204
- SessionStart: buildSessionStartHook(opts),
205
- UserPromptSubmit: buildUserPromptSubmitHook(opts),
206
- PreToolUse: buildPreToolUseHook(opts),
207
- Stop: buildStopHook(opts),
208
- };
209
- const pathFor = (e) => resolve(hookDir, SCRIPT_NAMES[e]);
210
- const readIf = async (p) => {
211
- try {
212
- return await readFile(p, 'utf-8');
213
- }
214
- catch {
215
- return '';
216
- }
217
- };
218
- let anyExisting = false;
219
- let allCurrent = true;
220
- for (const e of events) {
221
- const prev = await readIf(pathFor(e));
222
- if (prev)
223
- anyExisting = true;
224
- if (prev !== scriptFor[e] + '\n')
225
- allCurrent = false;
226
- }
227
- const scripts = allCurrent ? 'unchanged' : anyExisting ? 'upgraded' : 'written';
228
- if (scripts !== 'unchanged') {
229
- for (const e of events) {
230
- await writeFile(pathFor(e), scriptFor[e] + '\n', 'utf-8');
231
- await chmod(pathFor(e), 0o755).catch(() => { });
232
- }
233
- }
234
- // Merge the settings fragment (upgrade-safe: prior ambient groups replaced).
235
- const settingsPath = resolve(projectDir, SETTINGS_FILE);
236
- const settingsExisted = existsSync(settingsPath);
237
- let existingSettings = {};
238
- if (settingsExisted) {
239
- try {
240
- existingSettings = JSON.parse(await readFile(settingsPath, 'utf-8'));
241
- }
242
- catch {
243
- existingSettings = {};
244
- } // corrupt/empty file → start fresh (still non-destructive to hooks we add)
245
- }
246
- // Portable installs (committed hooks) reference $CLAUDE_PROJECT_DIR so the
247
- // settings work at any checkout location; local installs use absolute paths.
248
- // These are SCRIPT PATHS — buildAmbientSettingsHooks wraps them as
249
- // `node "<path>"`, so the portable var sits inside the double quotes and is
250
- // expanded by the shell at hook time.
251
- const commandFor = (e) => options.portable ? `$CLAUDE_PROJECT_DIR/${HOOK_DIR}/${SCRIPT_NAMES[e]}` : pathFor(e);
252
- const { settings, changed } = mergeAmbientSettings(existingSettings, {
253
- sessionStart: commandFor('SessionStart'),
254
- userPromptSubmit: commandFor('UserPromptSubmit'),
255
- preToolUse: commandFor('PreToolUse'),
256
- stop: commandFor('Stop'),
257
- });
258
- let settingsStatus;
259
- if (!settingsExisted)
260
- settingsStatus = 'written';
261
- else if (!changed)
262
- settingsStatus = 'unchanged';
263
- else
264
- settingsStatus = 'merged';
265
- if (settingsStatus !== 'unchanged') {
266
- await mkdir(resolve(projectDir, '.claude'), { recursive: true });
267
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
268
- }
269
- return {
270
- sessionStartPath: pathFor('SessionStart'),
271
- promptSubmitPath: pathFor('UserPromptSubmit'),
272
- preToolUsePath: pathFor('PreToolUse'),
273
- stopPath: pathFor('Stop'),
274
- scripts,
275
- settings: settingsStatus,
276
- };
277
- }
278
- //# sourceMappingURL=ambient-hooks.js.map
1
+ import{existsSync as v}from"node:fs";import{readFile as E,writeFile as y,chmod as N,mkdir as A}from"node:fs/promises";import{resolve as S}from"node:path";const O="v4",H="ambient-recall",w=`npx @cachly-dev/mcp-server@latest ${H}`;function P(t){return t.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}function d(t,o){const e=o.cliCommand??w;return["#!/usr/bin/env node",`// cachly Ambient Recall \u2014 ${t} ${O}`,"// Pushes relevant memory into context automatically. Cross-platform Node hook","// (no shell script \u2014 runs identically on Windows/macOS/Linux). Never blocks","// the agent: every failure path exits 0 with no output (graceful degrade).","import { spawn } from 'node:child_process';",`process.env.CACHLY_BRAIN_INSTANCE_ID = '${P(o.instanceId)}';`,"if (!process.env.CACHLY_JWT && process.env.CACHLY_API_KEY) process.env.CACHLY_JWT = process.env.CACHLY_API_KEY;",`process.env.CACHLY_HOOK_EVENT = '${P(t)}';`,"try {",` const child = spawn('${P(e)}', { shell: true, stdio: ['inherit', 'inherit', 'ignore'] });`," child.on('error', () => process.exit(0));"," child.on('close', () => process.exit(0));","} catch {"," process.exit(0);","}"].join(`
2
+ `)}function L(t){return d("SessionStart",t)}function R(t){return d("UserPromptSubmit",t)}function $(t){return d("PreToolUse",t)}function M(t){return d("Stop",t)}const Y={SessionStart:30,UserPromptSubmit:10,PreToolUse:10,Stop:60},F="Edit|Write|MultiEdit|NotebookEdit";function J(t){const o=(c,n,r)=>({...r?{matcher:r}:{},hooks:[{type:"command",command:`node "${n}"`,timeout:Y[c]}]}),e={SessionStart:[o("SessionStart",t.sessionStart)],UserPromptSubmit:[o("UserPromptSubmit",t.userPromptSubmit)]};return t.preToolUse&&(e.PreToolUse=[o("PreToolUse",t.preToolUse,F)]),t.stop&&(e.Stop=[o("Stop",t.stop)]),e}const C=".claude/hooks",j=".claude/settings.json",k={SessionStart:"cachly-ambient-session-start.mjs",UserPromptSubmit:"cachly-ambient-prompt-submit.mjs",PreToolUse:"cachly-ambient-pre-tool.mjs",Stop:"cachly-ambient-stop.mjs"},K=".claude/hooks/cachly-ambient-";function B(t,o){const e=t.hooks??[];return e.length>0&&e.every(c=>{const n=c.command??"";return n.includes(K)||o.has(n)||[...o].some(r=>n.includes(`"${r}"`))})}function D(t,o){const e={...t,hooks:{...t.hooks??{}}},c=e.hooks,n=J(o),r=new Set([o.sessionStart,o.userPromptSubmit,o.preToolUse,o.stop].filter(a=>!!a));for(const[a,i]of Object.entries(n)){const f=(Array.isArray(c[a])?c[a]:[]).filter(m=>!B(m,r));c[a]=[...f,...i]}const l=JSON.stringify(e)!==JSON.stringify(t);return{settings:e,changed:l}}async function q(t,o,e,c={}){const n=S(t,C);await A(n,{recursive:!0});const r={instanceId:o,apiKey:e},l=["SessionStart","UserPromptSubmit","PreToolUse","Stop"],a={SessionStart:L(r),UserPromptSubmit:R(r),PreToolUse:$(r),Stop:M(r)},i=s=>S(n,k[s]),f=async s=>{try{return await E(s,"utf-8")}catch{return""}};let m=!1,g=!0;for(const s of l){const _=await f(i(s));_&&(m=!0),_!==a[s]+`
3
+ `&&(g=!1)}const T=g?"unchanged":m?"upgraded":"written";if(T!=="unchanged")for(const s of l)await y(i(s),a[s]+`
4
+ `,"utf-8"),await N(i(s),493).catch(()=>{});const h=S(t,j),U=v(h);let b={};if(U)try{b=JSON.parse(await E(h,"utf-8"))}catch{b={}}const p=s=>c.portable?`$CLAUDE_PROJECT_DIR/${C}/${k[s]}`:i(s),{settings:x,changed:I}=D(b,{sessionStart:p("SessionStart"),userPromptSubmit:p("UserPromptSubmit"),preToolUse:p("PreToolUse"),stop:p("Stop")});let u;return U?I?u="merged":u="unchanged":u="written",u!=="unchanged"&&(await A(S(t,".claude"),{recursive:!0}),await y(h,JSON.stringify(x,null,2)+`
5
+ `,"utf-8")),{sessionStartPath:i("SessionStart"),promptSubmitPath:i("UserPromptSubmit"),preToolUsePath:i("PreToolUse"),stopPath:i("Stop"),scripts:T,settings:u}}export{H as AMBIENT_CLI_SUBCOMMAND,O as AMBIENT_HOOK_VERSION,F as PRE_TOOL_USE_MATCHER,J as buildAmbientSettingsHooks,$ as buildPreToolUseHook,L as buildSessionStartHook,M as buildStopHook,R as buildUserPromptSubmitHook,q as installAmbientHooks,D as mergeAmbientSettings};
@@ -1,95 +1,6 @@
1
- // Ambient Recall (Phase 4 Ausbau) — the net-token ledger.
2
- //
3
- // Roadmap §6.2: the whole feature is only honest if injected tokens (cost, paid
4
- // now) are weighed against prevented tokens (a wrong path a lesson averted).
5
- // This module persists per-turn records as JSONL (~/.cachly/ambient-ledger.jsonl)
6
- // so `ambient-stats` can show the NET number — even when it is negative — and
7
- // the CLI can auto-backoff (§6.3 guardrail 3) when the recent window is net-red.
8
- //
9
- // Prevented-credit in v1 is agent-reported: the injected context carries a tiny
10
- // footer inviting the agent to run `ambient-credit <tokens>` when a recalled
11
- // lesson actually changed its path. Self-reported ≠ perfect, but it is the only
12
- // client-side signal that exists before the server-side dashboard (v2) — and it
13
- // is what makes auto-backoff meaningful instead of permanently red.
14
- //
15
- // Everything here is best-effort: a broken ledger must never break a hook.
16
- import { appendFile, readFile, rename, writeFile } from 'node:fs/promises';
17
- import { mkdir } from 'node:fs/promises';
18
- import { homedir } from 'node:os';
19
- import { dirname, resolve } from 'node:path';
20
- /** Default ledger location; overridable for tests via explicit path args. */
21
- export function defaultLedgerPath() {
22
- return resolve(homedir(), '.cachly', 'ambient-ledger.jsonl');
23
- }
24
- /** One JSONL line for an entry. Pure. */
25
- export function serializeEntry(e) {
26
- return JSON.stringify({
27
- ts: e.ts,
28
- event: e.event,
29
- injected: Math.max(0, Math.round(e.injected)),
30
- prevented: Math.max(0, Math.round(e.prevented)),
31
- ...(e.note ? { note: e.note.slice(0, 200) } : {}),
32
- });
33
- }
34
- /** Parse ledger text into entries, silently skipping corrupt lines. Pure. */
35
- export function parseLedger(text) {
36
- const out = [];
37
- for (const line of text.split('\n')) {
38
- const t = line.trim();
39
- if (!t)
40
- continue;
41
- try {
42
- const e = JSON.parse(t);
43
- if (typeof e !== 'object' || e === null)
44
- continue;
45
- const injected = Number(e.injected);
46
- const prevented = Number(e.prevented);
47
- if (!Number.isFinite(injected) || !Number.isFinite(prevented))
48
- continue;
49
- out.push({
50
- ts: typeof e.ts === 'string' ? e.ts : '',
51
- event: typeof e.event === 'string' ? e.event : 'unknown',
52
- injected: Math.max(0, injected),
53
- prevented: Math.max(0, prevented),
54
- ...(typeof e.note === 'string' ? { note: e.note } : {}),
55
- });
56
- }
57
- catch {
58
- // corrupt line (partial write, manual edit) → skip, never throw
59
- }
60
- }
61
- return out;
62
- }
63
- // Rotation keeps the ledger bounded: hooks run on every prompt, and an
64
- // unbounded append-only file would eventually slow every turn down.
65
- const MAX_LEDGER_BYTES = 256 * 1024;
66
- const KEEP_LINES_ON_ROTATE = 400;
67
- /** Append an entry; auto-creates the directory, auto-rotates. Never throws. */
68
- export async function appendLedgerEntry(entry, path = defaultLedgerPath()) {
69
- try {
70
- await mkdir(dirname(path), { recursive: true });
71
- await appendFile(path, serializeEntry(entry) + '\n', 'utf-8');
72
- const text = await readFile(path, 'utf-8');
73
- if (Buffer.byteLength(text, 'utf-8') > MAX_LEDGER_BYTES) {
74
- const lines = text.split('\n').filter(Boolean);
75
- const kept = lines.slice(-KEEP_LINES_ON_ROTATE).join('\n') + '\n';
76
- // Write-then-rename so a crash mid-rotation never truncates to nothing.
77
- const tmp = path + '.tmp';
78
- await writeFile(tmp, kept, 'utf-8');
79
- await rename(tmp, path);
80
- }
81
- }
82
- catch {
83
- // Ledger is telemetry — never let it break a hook or a commit.
84
- }
85
- }
86
- /** Read all entries; missing/corrupt file → []. Never throws. */
87
- export async function readLedger(path = defaultLedgerPath()) {
88
- try {
89
- return parseLedger(await readFile(path, 'utf-8'));
90
- }
91
- catch {
92
- return [];
93
- }
94
- }
95
- //# sourceMappingURL=ambient-ledger.js.map
1
+ import{appendFile as u,readFile as s,rename as f,writeFile as p}from"node:fs/promises";import{mkdir as d}from"node:fs/promises";import{homedir as m}from"node:os";import{dirname as l,resolve as y}from"node:path";function a(){return y(m(),".cachly","ambient-ledger.jsonl")}function v(t){return JSON.stringify({ts:t.ts,event:t.event,injected:Math.max(0,Math.round(t.injected)),prevented:Math.max(0,Math.round(t.prevented)),...t.note?{note:t.note.slice(0,200)}:{}})}function g(t){const n=[];for(const i of t.split(`
2
+ `)){const o=i.trim();if(o)try{const e=JSON.parse(o);if(typeof e!="object"||e===null)continue;const r=Number(e.injected),c=Number(e.prevented);if(!Number.isFinite(r)||!Number.isFinite(c))continue;n.push({ts:typeof e.ts=="string"?e.ts:"",event:typeof e.event=="string"?e.event:"unknown",injected:Math.max(0,r),prevented:Math.max(0,c),...typeof e.note=="string"?{note:e.note}:{}})}catch{}}return n}const h=256*1024,x=400;async function b(t,n=a()){try{await d(l(n),{recursive:!0}),await u(n,v(t)+`
3
+ `,"utf-8");const i=await s(n,"utf-8");if(Buffer.byteLength(i,"utf-8")>h){const e=i.split(`
4
+ `).filter(Boolean).slice(-x).join(`
5
+ `)+`
6
+ `,r=n+".tmp";await p(r,e,"utf-8"),await f(r,n)}}catch{}}async function L(t=a()){try{return g(await s(t,"utf-8"))}catch{return[]}}export{b as appendLedgerEntry,a as defaultLedgerPath,g as parseLedger,L as readLedger,v as serializeEntry};
@@ -1,67 +1 @@
1
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { resolve } from 'node:path';
4
- import { emptyMemory } from './ambient-recall.js';
5
- const MEMORY_FILE_NAME = 'ambient-memory.json';
6
- const MAX_MEMORY_BYTES = 64 * 1024;
7
- function defaultAmbientMemoryDir() {
8
- return resolve(homedir(), '.cachly');
9
- }
10
- function isFiniteNumberRecord(value) {
11
- if (typeof value !== 'object' || value === null || Array.isArray(value))
12
- return false;
13
- return Object.values(value).every((n) => typeof n === 'number' && Number.isFinite(n));
14
- }
15
- function isValidMemory(value) {
16
- if (typeof value !== 'object' || value === null)
17
- return false;
18
- const m = value;
19
- if (typeof m.turn !== 'number' || !Number.isFinite(m.turn))
20
- return false;
21
- if (!isFiniteNumberRecord(m.lastInjectedTurn))
22
- return false;
23
- if (!Array.isArray(m.injectionTurns))
24
- return false;
25
- if (!m.injectionTurns.every((n) => typeof n === 'number' && Number.isFinite(n)))
26
- return false;
27
- return true;
28
- }
29
- /**
30
- * Loads the persisted {@link RecallMemory} from `<dir>/ambient-memory.json`.
31
- * Falls back to {@link emptyMemory} when the file is missing, corrupt, larger
32
- * than 64KB, or structurally invalid. Never throws.
33
- */
34
- export function loadAmbientMemory(dir = defaultAmbientMemoryDir()) {
35
- try {
36
- const raw = readFileSync(resolve(dir, MEMORY_FILE_NAME), 'utf-8');
37
- if (Buffer.byteLength(raw, 'utf-8') > MAX_MEMORY_BYTES)
38
- return emptyMemory();
39
- const parsed = JSON.parse(raw);
40
- if (!isValidMemory(parsed))
41
- return emptyMemory();
42
- return {
43
- turn: parsed.turn,
44
- lastInjectedTurn: { ...parsed.lastInjectedTurn },
45
- injectionTurns: [...parsed.injectionTurns],
46
- };
47
- }
48
- catch {
49
- return emptyMemory();
50
- }
51
- }
52
- /**
53
- * Persists `memory` to `<dir>/ambient-memory.json` via write-then-rename so a
54
- * crash mid-write never corrupts the file for the next read. Never throws.
55
- */
56
- export function saveAmbientMemory(memory, dir = defaultAmbientMemoryDir()) {
57
- try {
58
- mkdirSync(dir, { recursive: true });
59
- const path = resolve(dir, MEMORY_FILE_NAME);
60
- const tmp = `${path}.tmp`;
61
- writeFileSync(tmp, JSON.stringify(memory), 'utf-8');
62
- renameSync(tmp, path);
63
- }
64
- catch {
65
- }
66
- }
67
- //# sourceMappingURL=ambient-memory.js.map
1
+ import{mkdirSync as s,readFileSync as c,renameSync as m,writeFileSync as y}from"node:fs";import{homedir as a}from"node:os";import{resolve as n}from"node:path";import{emptyMemory as i}from"./ambient-recall.js";const u="ambient-memory.json",p=64*1024;function f(){return n(a(),".cachly")}function l(e){return typeof e!="object"||e===null||Array.isArray(e)?!1:Object.values(e).every(r=>typeof r=="number"&&Number.isFinite(r))}function b(e){if(typeof e!="object"||e===null)return!1;const r=e;return!(typeof r.turn!="number"||!Number.isFinite(r.turn)||!l(r.lastInjectedTurn)||!Array.isArray(r.injectionTurns)||!r.injectionTurns.every(t=>typeof t=="number"&&Number.isFinite(t)))}function T(e=f()){try{const r=c(n(e,u),"utf-8");if(Buffer.byteLength(r,"utf-8")>p)return i();const t=JSON.parse(r);return b(t)?{turn:t.turn,lastInjectedTurn:{...t.lastInjectedTurn},injectionTurns:[...t.injectionTurns]}:i()}catch{return i()}}function F(e,r=f()){try{s(r,{recursive:!0});const t=n(r,u),o=`${t}.tmp`;y(o,JSON.stringify(e),"utf-8"),m(o,t)}catch{}}export{T as loadAmbientMemory,F as saveAmbientMemory};