@mnemonik/claude-code-hooks 0.1.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/dist/hook.d.ts +12 -0
- package/dist/hook.js +357 -0
- package/dist/hook.js.map +1 -0
- package/dist/install.d.ts +17 -0
- package/dist/install.js +100 -0
- package/dist/install.js.map +1 -0
- package/dist/lib/env.d.ts +33 -0
- package/dist/lib/env.js +106 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/http.d.ts +29 -0
- package/dist/lib/http.js +63 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/shellTokens.d.ts +48 -0
- package/dist/lib/shellTokens.js +181 -0
- package/dist/lib/shellTokens.js.map +1 -0
- package/dist/lib/types.d.ts +81 -0
- package/dist/lib/types.js +16 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +30 -0
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mnemonik forcing-functions dispatcher for Claude Code hooks.
|
|
4
|
+
*
|
|
5
|
+
* Single Node script handling PreToolUse on Edit/Write/NotebookEdit/Bash and
|
|
6
|
+
* PostToolUse on Edit/Write/NotebookEdit. Reads Claude Code's hook payload on
|
|
7
|
+
* stdin, writes a decision JSON on stdout, exits 0 on success / 0 on
|
|
8
|
+
* fail-open / 0 on bypass. Never exits non-zero in advisory mode.
|
|
9
|
+
*
|
|
10
|
+
* Wired into .claude/settings.json by `mnemonik-claude-code-hooks install`.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mnemonik forcing-functions dispatcher for Claude Code hooks.
|
|
4
|
+
*
|
|
5
|
+
* Single Node script handling PreToolUse on Edit/Write/NotebookEdit/Bash and
|
|
6
|
+
* PostToolUse on Edit/Write/NotebookEdit. Reads Claude Code's hook payload on
|
|
7
|
+
* stdin, writes a decision JSON on stdout, exits 0 on success / 0 on
|
|
8
|
+
* fail-open / 0 on bypass. Never exits non-zero in advisory mode.
|
|
9
|
+
*
|
|
10
|
+
* Wired into .claude/settings.json by `mnemonik-claude-code-hooks install`.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
14
|
+
import { stdin, stdout, stderr, exit } from 'node:process';
|
|
15
|
+
import { isNonInteractive, parseBypass, resolveConfig } from './lib/env.js';
|
|
16
|
+
import { fetchSnapshot, reportBypass } from './lib/http.js';
|
|
17
|
+
import { BUILT_IN_DESTRUCTIVE, matchCommand, tokenize } from './lib/shellTokens.js';
|
|
18
|
+
const POLICY_DEDUP_WINDOW_MS = 60_000;
|
|
19
|
+
const MUST_CONFIRM_DEDUP_WINDOW_MS = 60_000;
|
|
20
|
+
// ─── Decision writers ──────────────────────────────────────────────────────
|
|
21
|
+
function writeDecision(decision) {
|
|
22
|
+
if (decision !== null) {
|
|
23
|
+
stdout.write(JSON.stringify(decision));
|
|
24
|
+
}
|
|
25
|
+
exit(0);
|
|
26
|
+
}
|
|
27
|
+
function writeAllow() {
|
|
28
|
+
exit(0);
|
|
29
|
+
}
|
|
30
|
+
function writeAdvisoryNote(message) {
|
|
31
|
+
stderr.write(`mnemonik-hook: ${message}\n`);
|
|
32
|
+
exit(0);
|
|
33
|
+
}
|
|
34
|
+
// ─── stdin reader ──────────────────────────────────────────────────────────
|
|
35
|
+
async function readStdin() {
|
|
36
|
+
return new Promise((res) => {
|
|
37
|
+
let buf = '';
|
|
38
|
+
stdin.setEncoding('utf8');
|
|
39
|
+
stdin.on('data', (chunk) => {
|
|
40
|
+
buf += chunk;
|
|
41
|
+
// Soft cap: if Claude ever ships a huge transcript we don't want to OOM.
|
|
42
|
+
if (buf.length > 1_048_576) {
|
|
43
|
+
res(null);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
stdin.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
res(JSON.parse(buf));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
res(null);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
stdin.on('error', () => res(null));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// ─── Edit / Write / NotebookEdit gate ──────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Heuristic soft-pass for trivial Edit-tool changes — the dispatcher's own
|
|
60
|
+
* check, not the agent's claim. Only applied to the Edit tool (NotebookEdit
|
|
61
|
+
* and Write don't surface a diff that lets us measure triviality).
|
|
62
|
+
*
|
|
63
|
+
* Trivial means both old_string and new_string are:
|
|
64
|
+
* - single-line (no embedded newlines),
|
|
65
|
+
* - ≤ 100 chars,
|
|
66
|
+
* - within 40% of each other in length.
|
|
67
|
+
*
|
|
68
|
+
* This is the kind of edit where forcing file_context buys nothing — fixing
|
|
69
|
+
* a typo in a comment, swapping a single identifier, tweaking whitespace.
|
|
70
|
+
* Anything multi-line or large enough to plausibly change behaviour fails
|
|
71
|
+
* the heuristic and goes through the normal gate.
|
|
72
|
+
*/
|
|
73
|
+
function isTrivialEdit(toolName, toolInput) {
|
|
74
|
+
if (toolName !== 'Edit')
|
|
75
|
+
return false;
|
|
76
|
+
if (!toolInput)
|
|
77
|
+
return false;
|
|
78
|
+
const oldStr = typeof toolInput.old_string === 'string' ? toolInput.old_string : null;
|
|
79
|
+
const newStr = typeof toolInput.new_string === 'string' ? toolInput.new_string : null;
|
|
80
|
+
if (oldStr === null || newStr === null)
|
|
81
|
+
return false;
|
|
82
|
+
if (oldStr.includes('\n') || newStr.includes('\n'))
|
|
83
|
+
return false;
|
|
84
|
+
if (oldStr.length === 0 || newStr.length === 0)
|
|
85
|
+
return false;
|
|
86
|
+
if (oldStr.length > 100 || newStr.length > 100)
|
|
87
|
+
return false;
|
|
88
|
+
const longer = Math.max(oldStr.length, newStr.length);
|
|
89
|
+
const shorter = Math.min(oldStr.length, newStr.length);
|
|
90
|
+
if (shorter / longer < 0.6)
|
|
91
|
+
return false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function resolveTargetFile(toolName, toolInput, cwd) {
|
|
95
|
+
if (!toolInput)
|
|
96
|
+
return null;
|
|
97
|
+
// Edit / Write expose `file_path`; NotebookEdit uses `notebook_path`. We
|
|
98
|
+
// accept both so the same dispatcher works across tool variants.
|
|
99
|
+
const raw = typeof toolInput.file_path === 'string'
|
|
100
|
+
? toolInput.file_path
|
|
101
|
+
: typeof toolInput.notebook_path === 'string'
|
|
102
|
+
? toolInput.notebook_path
|
|
103
|
+
: null;
|
|
104
|
+
if (!raw)
|
|
105
|
+
return null;
|
|
106
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
107
|
+
}
|
|
108
|
+
async function handlePreEdit(input, config) {
|
|
109
|
+
const mode = config.forcingFunctions.preEditGate;
|
|
110
|
+
if (mode === 'off')
|
|
111
|
+
writeAllow();
|
|
112
|
+
const target = resolveTargetFile(input.tool_name ?? '', input.tool_input, input.cwd);
|
|
113
|
+
// New file → nothing to ground on. The agent can't have read prior context
|
|
114
|
+
// for a path that doesn't exist yet.
|
|
115
|
+
if (target && !existsSync(target))
|
|
116
|
+
writeAllow();
|
|
117
|
+
// Trivial edit soft-pass — measured from the diff itself, not the agent's
|
|
118
|
+
// claim. Cheap & conservative: only the Edit tool with a small single-line
|
|
119
|
+
// delta qualifies. Saves a server round-trip on every typo / rename.
|
|
120
|
+
if (isTrivialEdit(input.tool_name ?? '', input.tool_input))
|
|
121
|
+
writeAllow();
|
|
122
|
+
if (!config.apiKey) {
|
|
123
|
+
writeAdvisoryNote('MNEMONIK_API_KEY missing — fail-open. Run mnemonik-claude-code-hooks install.');
|
|
124
|
+
}
|
|
125
|
+
const snapshot = await fetchSnapshot({
|
|
126
|
+
server: config.server,
|
|
127
|
+
apiKey: config.apiKey,
|
|
128
|
+
cwd: input.cwd,
|
|
129
|
+
claudeSessionId: input.session_id,
|
|
130
|
+
});
|
|
131
|
+
if (!snapshot) {
|
|
132
|
+
writeAdvisoryNote('mnemonik server unreachable — fail-open.');
|
|
133
|
+
}
|
|
134
|
+
// No active session means the agent hasn't bootstrapped Mnemonik yet. We
|
|
135
|
+
// never block under that condition — bootstrap-first is enforced by other
|
|
136
|
+
// mechanisms.
|
|
137
|
+
if (!snapshot.ok)
|
|
138
|
+
writeAllow();
|
|
139
|
+
// Coverage check uses absolute path equality. The server stores whatever
|
|
140
|
+
// path the agent passed to file_context, so we accept either absolute or
|
|
141
|
+
// cwd-relative form.
|
|
142
|
+
if (target) {
|
|
143
|
+
const relative = target.startsWith(input.cwd) ? target.slice(input.cwd.length + 1) : target;
|
|
144
|
+
const covered = snapshot.coveredFiles.includes(target) || snapshot.coveredFiles.includes(relative);
|
|
145
|
+
if (covered)
|
|
146
|
+
writeAllow();
|
|
147
|
+
}
|
|
148
|
+
if (mode === 'advisory') {
|
|
149
|
+
// Advisory: tell the agent what's missing but don't block.
|
|
150
|
+
const additionalContext = target
|
|
151
|
+
? [
|
|
152
|
+
`Mnemonik: file_context has not been called for "${target}" in this session.`,
|
|
153
|
+
'Recommended: call mnemonik.file_context({ filePaths: ["' +
|
|
154
|
+
target +
|
|
155
|
+
'"] }) before editing — it surfaces past bugs, decisions, and gotchas for the file.',
|
|
156
|
+
'This message will become an error when forcing-functions is set to "enforce" (currently advisory).',
|
|
157
|
+
].join(' ')
|
|
158
|
+
: 'Mnemonik: this edit was not preceded by a file_context call. Advisory only.';
|
|
159
|
+
const decision = {
|
|
160
|
+
hookSpecificOutput: {
|
|
161
|
+
hookEventName: 'PreToolUse',
|
|
162
|
+
permissionDecision: 'allow',
|
|
163
|
+
additionalContext,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
writeDecision(decision);
|
|
167
|
+
}
|
|
168
|
+
// mode === 'enforce'
|
|
169
|
+
const denyReason = target
|
|
170
|
+
? [
|
|
171
|
+
'MNEMONIK_FILE_CONTEXT_REQUIRED',
|
|
172
|
+
`Edit blocked: file_context has not been called for ${target} in this session.`,
|
|
173
|
+
`Fix: call mnemonik.file_context({ filePaths: ["${target}"] }) first.`,
|
|
174
|
+
"Bypass: set MNEMONIK_BYPASS_HOOKS=1 + MNEMONIK_BYPASS_TYPE=<typo|formatting|regenerated|already_grounded|other> (logged; 'other' additionally requires MNEMONIK_BYPASS_DETAIL of 50-200 chars).",
|
|
175
|
+
].join('\n')
|
|
176
|
+
: 'MNEMONIK_FILE_CONTEXT_REQUIRED — no target file resolvable from tool_input.';
|
|
177
|
+
const decision = {
|
|
178
|
+
hookSpecificOutput: {
|
|
179
|
+
hookEventName: 'PreToolUse',
|
|
180
|
+
permissionDecision: 'deny',
|
|
181
|
+
permissionDecisionReason: denyReason,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
writeDecision(decision);
|
|
185
|
+
}
|
|
186
|
+
// ─── Bash gate ─────────────────────────────────────────────────────────────
|
|
187
|
+
async function handlePreBash(input, config) {
|
|
188
|
+
const mode = config.forcingFunctions.preBashGate;
|
|
189
|
+
if (mode === 'off')
|
|
190
|
+
writeAllow();
|
|
191
|
+
const command = typeof input.tool_input?.command === 'string' ? input.tool_input.command : '';
|
|
192
|
+
if (!command.trim())
|
|
193
|
+
writeAllow();
|
|
194
|
+
if (!config.apiKey) {
|
|
195
|
+
writeAdvisoryNote('MNEMONIK_API_KEY missing — fail-open. Run mnemonik-claude-code-hooks install.');
|
|
196
|
+
}
|
|
197
|
+
const snapshot = await fetchSnapshot({
|
|
198
|
+
server: config.server,
|
|
199
|
+
apiKey: config.apiKey,
|
|
200
|
+
cwd: input.cwd,
|
|
201
|
+
claudeSessionId: input.session_id,
|
|
202
|
+
});
|
|
203
|
+
if (!snapshot) {
|
|
204
|
+
writeAdvisoryNote('mnemonik server unreachable — fail-open.');
|
|
205
|
+
}
|
|
206
|
+
// Always include the built-in destructive set; project policies extend it.
|
|
207
|
+
// We only act on patterns from forbiddenCommand and mustConfirm — defaultMode
|
|
208
|
+
// is informational and doesn't gate.
|
|
209
|
+
const projectForbidden = snapshot.policies.forbiddenCommand.map((p) => p.pattern);
|
|
210
|
+
const projectMustConfirm = snapshot.policies.mustConfirm.map((p) => p.pattern);
|
|
211
|
+
const forbiddenPatterns = [...BUILT_IN_DESTRUCTIVE, ...projectForbidden];
|
|
212
|
+
const { commands } = tokenize(command);
|
|
213
|
+
let forbiddenHit = null;
|
|
214
|
+
let mustConfirmHit = null;
|
|
215
|
+
for (const tokens of commands) {
|
|
216
|
+
forbiddenHit = forbiddenHit ?? matchCommand(tokens, forbiddenPatterns);
|
|
217
|
+
mustConfirmHit = mustConfirmHit ?? matchCommand(tokens, projectMustConfirm);
|
|
218
|
+
if (forbiddenHit && mustConfirmHit)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
// No matches → nothing for the gate to do.
|
|
222
|
+
if (!forbiddenHit && !mustConfirmHit)
|
|
223
|
+
writeAllow();
|
|
224
|
+
// De-dup: if the agent JUST issued a manual policy({action:'check'}) for
|
|
225
|
+
// the same pattern, or just emitted a mustConfirm pause, don't nag again.
|
|
226
|
+
const now = snapshot.now;
|
|
227
|
+
const checkedRecently = (pattern) => {
|
|
228
|
+
const ts = snapshot.recentPolicyChecks[pattern];
|
|
229
|
+
return typeof ts === 'number' && now - ts < POLICY_DEDUP_WINDOW_MS;
|
|
230
|
+
};
|
|
231
|
+
const pausedRecently = typeof snapshot.mustConfirmPauseAt === 'number' &&
|
|
232
|
+
now - snapshot.mustConfirmPauseAt < MUST_CONFIRM_DEDUP_WINDOW_MS;
|
|
233
|
+
const hit = forbiddenHit ?? mustConfirmHit;
|
|
234
|
+
if (forbiddenHit && checkedRecently(forbiddenHit))
|
|
235
|
+
writeAllow();
|
|
236
|
+
if (mustConfirmHit && (checkedRecently(mustConfirmHit) || pausedRecently))
|
|
237
|
+
writeAllow();
|
|
238
|
+
if (mode === 'advisory') {
|
|
239
|
+
const kind = forbiddenHit ? 'forbiddenCommand' : 'mustConfirm';
|
|
240
|
+
const additionalContext = [
|
|
241
|
+
`Mnemonik policy hit (${kind}): pattern "${hit}" matched the command.`,
|
|
242
|
+
'Recommended: call mnemonik.policy({ action: "check", command: "' +
|
|
243
|
+
command +
|
|
244
|
+
'" }) to confirm before running, or emit a mustConfirm pause summarising the action and waiting for the user.',
|
|
245
|
+
'This message will become an error when forcing-functions is set to "enforce" (currently advisory).',
|
|
246
|
+
].join(' ');
|
|
247
|
+
const decision = {
|
|
248
|
+
hookSpecificOutput: {
|
|
249
|
+
hookEventName: 'PreToolUse',
|
|
250
|
+
permissionDecision: 'allow',
|
|
251
|
+
additionalContext,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
writeDecision(decision);
|
|
255
|
+
}
|
|
256
|
+
// mode === 'enforce'
|
|
257
|
+
const denyReason = [
|
|
258
|
+
'MNEMONIK_POLICY_GATE',
|
|
259
|
+
`Bash blocked: pattern "${hit}" requires a manual policy check or mustConfirm pause first.`,
|
|
260
|
+
'Fix: call mnemonik.policy({ action: "check", command }) and act on the response, or emit a mustConfirm summary and wait for the user.',
|
|
261
|
+
"Bypass: MNEMONIK_BYPASS_HOOKS=1 + MNEMONIK_BYPASS_TYPE=<typo|formatting|regenerated|already_grounded|other>; 'other' additionally requires MNEMONIK_BYPASS_DETAIL 50-200 chars.",
|
|
262
|
+
].join('\n');
|
|
263
|
+
const decision = {
|
|
264
|
+
hookSpecificOutput: {
|
|
265
|
+
hookEventName: 'PreToolUse',
|
|
266
|
+
permissionDecision: 'deny',
|
|
267
|
+
permissionDecisionReason: denyReason,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
writeDecision(decision);
|
|
271
|
+
}
|
|
272
|
+
// ─── PostToolUse: keep server's filesEdited fresh ──────────────────────────
|
|
273
|
+
async function handlePostEdit(input, config) {
|
|
274
|
+
// Best-effort track-ide-edit — fire-and-forget, always allow.
|
|
275
|
+
try {
|
|
276
|
+
if (!config.apiKey)
|
|
277
|
+
writeAllow();
|
|
278
|
+
const filePath = resolveTargetFile(input.tool_name ?? '', input.tool_input, input.cwd);
|
|
279
|
+
if (!filePath)
|
|
280
|
+
writeAllow();
|
|
281
|
+
const ac = new AbortController();
|
|
282
|
+
const timer = setTimeout(() => ac.abort(), 1500);
|
|
283
|
+
await fetch(`${config.server}/api/v1/session/track-ide-edit`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${config.apiKey}` },
|
|
286
|
+
body: JSON.stringify({ sessionId: input.session_id, filePath }),
|
|
287
|
+
signal: ac.signal,
|
|
288
|
+
}).catch(() => undefined);
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Never block on PostToolUse.
|
|
293
|
+
}
|
|
294
|
+
writeAllow();
|
|
295
|
+
}
|
|
296
|
+
// ─── Top-level dispatcher ──────────────────────────────────────────────────
|
|
297
|
+
async function dispatch(input) {
|
|
298
|
+
// CI / non-interactive — always allow, never block.
|
|
299
|
+
if (isNonInteractive())
|
|
300
|
+
writeAllow();
|
|
301
|
+
const bypass = parseBypass();
|
|
302
|
+
if (bypass.invalidReason) {
|
|
303
|
+
// Malformed bypass envelope: refuse to honour it AND tell the agent why
|
|
304
|
+
// via stderr so the bypass can't be silently broken.
|
|
305
|
+
stderr.write(`mnemonik-hook: invalid bypass envelope — ${bypass.invalidReason}\n`);
|
|
306
|
+
}
|
|
307
|
+
const config = await resolveConfig(input.cwd);
|
|
308
|
+
if (bypass.active && config.apiKey) {
|
|
309
|
+
void reportBypass({
|
|
310
|
+
server: config.server,
|
|
311
|
+
apiKey: config.apiKey,
|
|
312
|
+
cwd: input.cwd,
|
|
313
|
+
claudeSessionId: input.session_id,
|
|
314
|
+
tool: input.tool_name ?? 'unknown',
|
|
315
|
+
bypassType: bypass.type ?? 'unknown',
|
|
316
|
+
detail: bypass.detail,
|
|
317
|
+
});
|
|
318
|
+
writeAllow();
|
|
319
|
+
}
|
|
320
|
+
switch (input.hook_event_name) {
|
|
321
|
+
case 'PreToolUse': {
|
|
322
|
+
const tool = input.tool_name ?? '';
|
|
323
|
+
if (tool === 'Edit' || tool === 'Write' || tool === 'NotebookEdit') {
|
|
324
|
+
await handlePreEdit(input, config);
|
|
325
|
+
}
|
|
326
|
+
if (tool === 'Bash') {
|
|
327
|
+
await handlePreBash(input, config);
|
|
328
|
+
}
|
|
329
|
+
writeAllow();
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'PostToolUse': {
|
|
333
|
+
const tool = input.tool_name ?? '';
|
|
334
|
+
if (tool === 'Edit' || tool === 'Write' || tool === 'NotebookEdit') {
|
|
335
|
+
await handlePostEdit(input, config);
|
|
336
|
+
}
|
|
337
|
+
writeAllow();
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
// SessionStart / Stop / etc — no-op for now; reserved for future phases.
|
|
342
|
+
writeAllow();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
(async () => {
|
|
346
|
+
try {
|
|
347
|
+
const input = await readStdin();
|
|
348
|
+
if (!input)
|
|
349
|
+
writeAllow();
|
|
350
|
+
await dispatch(input);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
stderr.write(`mnemonik-hook: dispatcher error (fail-open) — ${err instanceof Error ? err.message : String(err)}\n`);
|
|
354
|
+
writeAllow();
|
|
355
|
+
}
|
|
356
|
+
})();
|
|
357
|
+
//# sourceMappingURL=hook.js.map
|
package/dist/hook.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AASpF,MAAM,sBAAsB,GAAG,MAAM,CAAC;AACtC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAE5C,8EAA8E;AAE9E,SAAS,aAAa,CAAC,QAAyD;IAC9E,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,CAAC;AACV,CAAC;AAED,SAAS,UAAU;IACjB,IAAI,CAAC,CAAC,CAAC,CAAC;AACV,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,CAAC,KAAK,CAAC,kBAAkB,OAAO,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,CAAC,CAAC,CAAC;AACV,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,SAAS;IACtB,OAAO,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACzB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACjC,GAAG,IAAI,KAAK,CAAC;YACb,yEAAyE;YACzE,IAAI,GAAG,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,CAAC,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACnB,IAAI,CAAC;gBACH,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAc,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,CAAC,IAAI,CAAC,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,SAAS,aAAa,CAAC,QAAgB,EAAE,SAA8C;IACrF,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,MAAM,MAAM,GAAG,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,MAAM,MAAM,GAAG,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACjE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7D,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,KAAK,CAAC;IAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,iBAAiB,CACxB,QAAgB,EAChB,SAA8C,EAC9C,GAAW;IAEX,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,GAAG,GACP,OAAO,SAAS,CAAC,SAAS,KAAK,QAAQ;QACrC,CAAC,CAAC,SAAS,CAAC,SAAS;QACrB,CAAC,CAAC,OAAO,SAAS,CAAC,aAAa,KAAK,QAAQ;YAC3C,CAAC,CAAC,SAAS,CAAC,aAAa;YACzB,CAAC,CAAC,IAAI,CAAC;IACb,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,KAAgB,EAAE,MAAkB;IAC/D,MAAM,IAAI,GAAG,MAAM,CAAC,gBAAgB,CAAC,WAAW,CAAC;IACjD,IAAI,IAAI,KAAK,KAAK;QAAE,UAAU,EAAE,CAAC;IAEjC,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAErF,2EAA2E;IAC3E,qCAAqC;IACrC,IAAI,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,UAAU,EAAE,CAAC;IAEhD,0EAA0E;IAC1E,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,aAAa,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC;QAAE,UAAU,EAAE,CAAC;IAEzE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,iBAAiB,CACf,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAO;QACtB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,eAAe,EAAE,KAAK,CAAC,UAAU;KAClC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,iBAAiB,CAAC,0CAA0C,CAAC,CAAC;IAChE,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,cAAc;IACd,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,UAAU,EAAE,CAAC;IAE/B,yEAAyE;IACzE,yEAAyE;IACzE,qBAAqB;IACrB,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC5F,MAAM,OAAO,GACX,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACrF,IAAI,OAAO;YAAE,UAAU,EAAE,CAAC;IAC5B,CAAC;IAED,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,2DAA2D;QAC3D,MAAM,iBAAiB,GAAG,MAAM;YAC9B,CAAC,CAAC;gBACE,mDAAmD,MAAM,oBAAoB;gBAC7E,yDAAyD;oBACvD,MAAM;oBACN,oFAAoF;gBACtF,oGAAoG;aACrG,CAAC,IAAI,CAAC,GAAG,CAAC;YACb,CAAC,CAAC,6EAA6E,CAAC;QAClF,MAAM,QAAQ,GAAuB;YACnC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,iBAAiB;aAClB;SACF,CAAC;QACF,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAED,qBAAqB;IACrB,MAAM,UAAU,GAAG,MAAM;QACvB,CAAC,CAAC;YACE,gCAAgC;YAChC,sDAAsD,MAAM,mBAAmB;YAC/E,kDAAkD,MAAM,cAAc;YACtE,iMAAiM;SAClM,CAAC,IAAI,CAAC,IAAI,CAAC;QACd,CAAC,CAAC,6EAA6E,CAAC;IAClF,MAAM,QAAQ,GAAuB;QACnC,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,UAAU;SACrC;KACF,CAAC;IACF,aAAa,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,aAAa,CAAC,KAAgB,EAAE,MAAkB;IAC/D,MAAM,IAAI,GAAG,MAAM,CAAC,gBAAgB,CAAC,WAAW,CAAC;IACjD,IAAI,IAAI,KAAK,KAAK;QAAE,UAAU,EAAE,CAAC;IAEjC,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,UAAU,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9F,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,CAAC;IAElC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,iBAAiB,CACf,+EAA+E,CAChF,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAO;QACtB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,eAAe,EAAE,KAAK,CAAC,UAAU;KAClC,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,iBAAiB,CAAC,0CAA0C,CAAC,CAAC;IAChE,CAAC;IAED,2EAA2E;IAC3E,8EAA8E;IAC9E,qCAAqC;IACrC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAClF,MAAM,kBAAkB,GAAG,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC/E,MAAM,iBAAiB,GAAG,CAAC,GAAG,oBAAoB,EAAE,GAAG,gBAAgB,CAAC,CAAC;IAEzE,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,YAAY,GAAkB,IAAI,CAAC;IACvC,IAAI,cAAc,GAAkB,IAAI,CAAC;IACzC,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,YAAY,GAAG,YAAY,IAAI,YAAY,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;QACvE,cAAc,GAAG,cAAc,IAAI,YAAY,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;QAC5E,IAAI,YAAY,IAAI,cAAc;YAAE,MAAM;IAC5C,CAAC;IAED,2CAA2C;IAC3C,IAAI,CAAC,YAAY,IAAI,CAAC,cAAc;QAAE,UAAU,EAAE,CAAC;IAEnD,yEAAyE;IACzE,0EAA0E;IAC1E,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;IACzB,MAAM,eAAe,GAAG,CAAC,OAAe,EAAW,EAAE;QACnD,MAAM,EAAE,GAAG,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAChD,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,GAAG,GAAG,EAAE,GAAG,sBAAsB,CAAC;IACrE,CAAC,CAAC;IACF,MAAM,cAAc,GAClB,OAAO,QAAQ,CAAC,kBAAkB,KAAK,QAAQ;QAC/C,GAAG,GAAG,QAAQ,CAAC,kBAAkB,GAAG,4BAA4B,CAAC;IAEnE,MAAM,GAAG,GAAG,YAAY,IAAI,cAAe,CAAC;IAC5C,IAAI,YAAY,IAAI,eAAe,CAAC,YAAY,CAAC;QAAE,UAAU,EAAE,CAAC;IAChE,IAAI,cAAc,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,IAAI,cAAc,CAAC;QAAE,UAAU,EAAE,CAAC;IAExF,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,aAAa,CAAC;QAC/D,MAAM,iBAAiB,GAAG;YACxB,wBAAwB,IAAI,eAAe,GAAG,wBAAwB;YACtE,iEAAiE;gBAC/D,OAAO;gBACP,8GAA8G;YAChH,oGAAoG;SACrG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACZ,MAAM,QAAQ,GAAuB;YACnC,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,OAAO;gBAC3B,iBAAiB;aAClB;SACF,CAAC;QACF,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAED,qBAAqB;IACrB,MAAM,UAAU,GAAG;QACjB,sBAAsB;QACtB,0BAA0B,GAAG,8DAA8D;QAC3F,uIAAuI;QACvI,iLAAiL;KAClL,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,MAAM,QAAQ,GAAuB;QACnC,kBAAkB,EAAE;YAClB,aAAa,EAAE,YAAY;YAC3B,kBAAkB,EAAE,MAAM;YAC1B,wBAAwB,EAAE,UAAU;SACrC;KACF,CAAC;IACF,aAAa,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,cAAc,CAAC,KAAgB,EAAE,MAAkB;IAChE,8DAA8D;IAC9D,IAAI,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,UAAU,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QACvF,IAAI,CAAC,QAAQ;YAAE,UAAU,EAAE,CAAC;QAC5B,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QACjD,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,gCAAgC,EAAE;YAC5D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,MAAO,EAAE,EAAE;YAC1F,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC;YAC/D,MAAM,EAAE,EAAE,CAAC,MAAM;SAClB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC1B,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,8BAA8B;IAChC,CAAC;IACD,UAAU,EAAE,CAAC;AACf,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,QAAQ,CAAC,KAAgB;IACtC,oDAAoD;IACpD,IAAI,gBAAgB,EAAE;QAAE,UAAU,EAAE,CAAC;IAErC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;IAC7B,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACzB,wEAAwE;QACxE,qDAAqD;QACrD,MAAM,CAAC,KAAK,CAAC,4CAA4C,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE9C,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnC,KAAK,YAAY,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,eAAe,EAAE,KAAK,CAAC,UAAU;YACjC,IAAI,EAAE,KAAK,CAAC,SAAS,IAAI,SAAS;YAClC,UAAU,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS;YACpC,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,UAAU,EAAE,CAAC;IACf,CAAC;IAED,QAAQ,KAAK,CAAC,eAAe,EAAE,CAAC;QAC9B,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC;YACnC,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBACnE,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACrC,CAAC;YACD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;gBACpB,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACrC,CAAC;YACD,UAAU,EAAE,CAAC;YACb,MAAM;QACR,CAAC;QACD,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC;YACnC,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBACnE,MAAM,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACtC,CAAC;YACD,UAAU,EAAE,CAAC;YACb,MAAM;QACR,CAAC;QACD;YACE,yEAAyE;YACzE,UAAU,EAAE,CAAC;IACjB,CAAC;AACH,CAAC;AAED,CAAC,KAAK,IAAI,EAAE;IACV,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,UAAU,EAAE,CAAC;QACzB,MAAM,QAAQ,CAAC,KAAM,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CACV,iDAAiD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CACtG,CAAC;QACF,UAAU,EAAE,CAAC;IACf,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `mnemonik-claude-code-hooks install` — wires the dispatcher into
|
|
4
|
+
* .claude/settings.json without clobbering the user's existing config.
|
|
5
|
+
*
|
|
6
|
+
* What it does:
|
|
7
|
+
* 1. Resolves the dispatcher path (this package's dist/hook.js).
|
|
8
|
+
* 2. Reads (or creates) .claude/settings.json at the cwd.
|
|
9
|
+
* 3. Merges PreToolUse/PostToolUse entries pointing at `node <dispatcher>`,
|
|
10
|
+
* removing any prior mnemonik-managed entries first so re-running the
|
|
11
|
+
* installer is idempotent.
|
|
12
|
+
* 4. Writes the file back, preserving any unrelated user config.
|
|
13
|
+
*
|
|
14
|
+
* No npm dependencies — uses Node 20 builtins only. Safe to run from any
|
|
15
|
+
* project root; never touches files outside `.claude/`.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `mnemonik-claude-code-hooks install` — wires the dispatcher into
|
|
4
|
+
* .claude/settings.json without clobbering the user's existing config.
|
|
5
|
+
*
|
|
6
|
+
* What it does:
|
|
7
|
+
* 1. Resolves the dispatcher path (this package's dist/hook.js).
|
|
8
|
+
* 2. Reads (or creates) .claude/settings.json at the cwd.
|
|
9
|
+
* 3. Merges PreToolUse/PostToolUse entries pointing at `node <dispatcher>`,
|
|
10
|
+
* removing any prior mnemonik-managed entries first so re-running the
|
|
11
|
+
* installer is idempotent.
|
|
12
|
+
* 4. Writes the file back, preserving any unrelated user config.
|
|
13
|
+
*
|
|
14
|
+
* No npm dependencies — uses Node 20 builtins only. Safe to run from any
|
|
15
|
+
* project root; never touches files outside `.claude/`.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
const HOOK_TAG = 'mnemonik-claude-code-hooks';
|
|
22
|
+
function resolveDispatcherPath() {
|
|
23
|
+
// dist/install.js → dist/hook.js
|
|
24
|
+
const here = fileURLToPath(import.meta.url);
|
|
25
|
+
return resolve(dirname(here), 'hook.js');
|
|
26
|
+
}
|
|
27
|
+
function buildHookEntry(dispatcher) {
|
|
28
|
+
return {
|
|
29
|
+
type: 'command',
|
|
30
|
+
command: `node ${JSON.stringify(dispatcher)}`,
|
|
31
|
+
timeout: 5,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isMnemonikEntry(entry) {
|
|
35
|
+
// We tag both via the matcher and the dispatcher path so re-runs find us
|
|
36
|
+
// even if the user changed the matcher manually.
|
|
37
|
+
if (entry.matcher === HOOK_TAG)
|
|
38
|
+
return true;
|
|
39
|
+
return Boolean(entry.hooks?.some((h) => typeof h.command === 'string' && h.command.includes('claude-code-hooks')));
|
|
40
|
+
}
|
|
41
|
+
function patchHookGroup(groups, matcher, entry) {
|
|
42
|
+
const filtered = (groups ?? []).filter((g) => !isMnemonikEntry(g));
|
|
43
|
+
filtered.push({ matcher, hooks: [entry] });
|
|
44
|
+
return filtered;
|
|
45
|
+
}
|
|
46
|
+
async function readSettings(path) {
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
return {};
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(path, 'utf8');
|
|
51
|
+
return JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new Error(`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function writeSettings(path, settings) {
|
|
58
|
+
await mkdir(dirname(path), { recursive: true });
|
|
59
|
+
// Preserve a trailing newline since prettier-style users rely on it.
|
|
60
|
+
await writeFile(path, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
61
|
+
}
|
|
62
|
+
async function main() {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
65
|
+
const dispatcher = resolveDispatcherPath();
|
|
66
|
+
if (!existsSync(dispatcher)) {
|
|
67
|
+
console.error(`mnemonik-claude-code-hooks: dispatcher not found at ${dispatcher}. Did the package install correctly?`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
const settings = await readSettings(settingsPath);
|
|
71
|
+
const hooks = (settings.hooks ?? {});
|
|
72
|
+
const entry = buildHookEntry(dispatcher);
|
|
73
|
+
// PreToolUse — guard Edit / Write / NotebookEdit / Bash.
|
|
74
|
+
hooks.PreToolUse = patchHookGroup(hooks.PreToolUse, 'Edit|Write|NotebookEdit|Bash', entry);
|
|
75
|
+
// PostToolUse — keep filesEdited fresh after Edit / Write / NotebookEdit.
|
|
76
|
+
hooks.PostToolUse = patchHookGroup(hooks.PostToolUse, 'Edit|Write|NotebookEdit', entry);
|
|
77
|
+
settings.hooks = hooks;
|
|
78
|
+
await writeSettings(settingsPath, settings);
|
|
79
|
+
console.log([
|
|
80
|
+
`mnemonik-claude-code-hooks: wired into ${settingsPath}`,
|
|
81
|
+
` PreToolUse → Edit|Write|NotebookEdit|Bash`,
|
|
82
|
+
` PostToolUse → Edit|Write|NotebookEdit`,
|
|
83
|
+
` dispatcher → ${dispatcher}`,
|
|
84
|
+
``,
|
|
85
|
+
`Default mode is ADVISORY (no edits will be blocked). To switch to enforce,`,
|
|
86
|
+
`add to .mnemonik.json:`,
|
|
87
|
+
` { "forcingFunctions": { "preEditGate": "enforce" } }`,
|
|
88
|
+
``,
|
|
89
|
+
`Bypass for trivial edits:`,
|
|
90
|
+
` MNEMONIK_BYPASS_HOOKS=1 MNEMONIK_BYPASS_TYPE=typo claude ...`,
|
|
91
|
+
].join('\n'));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
main()
|
|
95
|
+
.then((code) => process.exit(code))
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
console.error(`mnemonik-claude-code-hooks: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,QAAQ,GAAG,4BAA4B,CAAC;AAsB9C,SAAS,qBAAqB;IAC5B,iCAAiC;IACjC,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB;IACxC,OAAO;QACL,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,QAAQ,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE;QAC7C,OAAO,EAAE,CAAC;KACX,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAwB;IAC/C,yEAAyE;IACzE,iDAAiD;IACjD,IAAI,KAAK,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,OAAO,CACZ,KAAK,CAAC,KAAK,EAAE,IAAI,CACf,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAChF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CACrB,MAAuC,EACvC,OAAe,EACf,KAAyB;IAEzB,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAY;IACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC3C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,QAAwB;IACjE,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,qEAAqE;IACrE,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAC1E,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,qBAAqB,EAAE,CAAC;IAE3C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CACX,uDAAuD,UAAU,sCAAsC,CACxG,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,YAAY,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAyC,CAAC;IAE7E,MAAM,KAAK,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAEzC,yDAAyD;IACzD,KAAK,CAAC,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,UAAU,EAAE,8BAA8B,EAAE,KAAK,CAAC,CAAC;IAE3F,0EAA0E;IAC1E,KAAK,CAAC,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,WAAW,EAAE,yBAAyB,EAAE,KAAK,CAAC,CAAC;IAExF,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,MAAM,aAAa,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAE5C,OAAO,CAAC,GAAG,CACT;QACE,0CAA0C,YAAY,EAAE;QACxD,8CAA8C;QAC9C,yCAAyC;QACzC,mBAAmB,UAAU,EAAE;QAC/B,EAAE;QACF,4EAA4E;QAC5E,wBAAwB;QACxB,wDAAwD;QACxD,EAAE;QACF,2BAA2B;QAC3B,gEAAgE;KACjE,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,CAAC;AACX,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KAClC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CACX,+BAA+B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAClF,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment helpers: CI detection, bypass parsing, config resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { type BypassEnvelope, type HookConfig } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* CI / non-interactive contexts must never block. We mirror the husky
|
|
7
|
+
* pre-push pattern already used elsewhere: any of these wins.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNonInteractive(env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parse the bypass envelope. Refuses any malformed combination — we'd rather
|
|
12
|
+
* fail-open silently than honour a typoed escape hatch indefinitely.
|
|
13
|
+
*
|
|
14
|
+
* Required when active:
|
|
15
|
+
* MNEMONIK_BYPASS_HOOKS=1
|
|
16
|
+
* MNEMONIK_BYPASS_TYPE=<typo|formatting|regenerated|already_grounded|other>
|
|
17
|
+
* MNEMONIK_BYPASS_DETAIL=<50–200 chars> (only when type=other)
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseBypass(env?: NodeJS.ProcessEnv): BypassEnvelope;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve hook config from layered sources, in priority order:
|
|
22
|
+
*
|
|
23
|
+
* 1. Env vars (MNEMONIK_API_KEY, MNEMONIK_SERVER) — let users override per-shell.
|
|
24
|
+
* 2. ~/.mnemonik/scanner.json — already the source of truth for the scanner
|
|
25
|
+
* daemon, so reusing it means users with the scanner working are wired
|
|
26
|
+
* for hooks for free.
|
|
27
|
+
* 3. <cwd>/.mnemonik.json — for the per-project `forcingFunctions` block.
|
|
28
|
+
*
|
|
29
|
+
* Missing API key returns `apiKey: null` rather than throwing — the dispatcher
|
|
30
|
+
* fail-opens with a stderr warning so a misconfigured machine never breaks
|
|
31
|
+
* the user's workflow.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveConfig(cwd: string, env?: NodeJS.ProcessEnv): Promise<HookConfig>;
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment helpers: CI detection, bypass parsing, config resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { DEFAULT_FORCING_FUNCTIONS, DEFAULT_SERVER, } from './types.js';
|
|
8
|
+
const VALID_BYPASS_TYPES = new Set([
|
|
9
|
+
'typo',
|
|
10
|
+
'formatting',
|
|
11
|
+
'regenerated',
|
|
12
|
+
'already_grounded',
|
|
13
|
+
'other',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* CI / non-interactive contexts must never block. We mirror the husky
|
|
17
|
+
* pre-push pattern already used elsewhere: any of these wins.
|
|
18
|
+
*/
|
|
19
|
+
export function isNonInteractive(env = process.env) {
|
|
20
|
+
if (env.CI === 'true')
|
|
21
|
+
return true;
|
|
22
|
+
if (env.GITHUB_ACTIONS === 'true')
|
|
23
|
+
return true;
|
|
24
|
+
if (env.MNEMONIK_NONINTERACTIVE === '1')
|
|
25
|
+
return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse the bypass envelope. Refuses any malformed combination — we'd rather
|
|
30
|
+
* fail-open silently than honour a typoed escape hatch indefinitely.
|
|
31
|
+
*
|
|
32
|
+
* Required when active:
|
|
33
|
+
* MNEMONIK_BYPASS_HOOKS=1
|
|
34
|
+
* MNEMONIK_BYPASS_TYPE=<typo|formatting|regenerated|already_grounded|other>
|
|
35
|
+
* MNEMONIK_BYPASS_DETAIL=<50–200 chars> (only when type=other)
|
|
36
|
+
*/
|
|
37
|
+
export function parseBypass(env = process.env) {
|
|
38
|
+
if (env.MNEMONIK_BYPASS_HOOKS !== '1') {
|
|
39
|
+
return { active: false, type: null, detail: null, invalidReason: null };
|
|
40
|
+
}
|
|
41
|
+
const rawType = (env.MNEMONIK_BYPASS_TYPE ?? '').trim().toLowerCase();
|
|
42
|
+
if (!rawType) {
|
|
43
|
+
return {
|
|
44
|
+
active: false,
|
|
45
|
+
type: null,
|
|
46
|
+
detail: null,
|
|
47
|
+
invalidReason: 'MNEMONIK_BYPASS_TYPE is required when MNEMONIK_BYPASS_HOOKS=1',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!VALID_BYPASS_TYPES.has(rawType)) {
|
|
51
|
+
return {
|
|
52
|
+
active: false,
|
|
53
|
+
type: null,
|
|
54
|
+
detail: null,
|
|
55
|
+
invalidReason: `MNEMONIK_BYPASS_TYPE must be one of ${[...VALID_BYPASS_TYPES].join('|')}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const type = rawType;
|
|
59
|
+
if (type === 'other') {
|
|
60
|
+
const detail = (env.MNEMONIK_BYPASS_DETAIL ?? '').trim();
|
|
61
|
+
if (detail.length < 50 || detail.length > 200) {
|
|
62
|
+
return {
|
|
63
|
+
active: false,
|
|
64
|
+
type: null,
|
|
65
|
+
detail: null,
|
|
66
|
+
invalidReason: "MNEMONIK_BYPASS_TYPE='other' requires MNEMONIK_BYPASS_DETAIL of 50–200 chars",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { active: true, type, detail, invalidReason: null };
|
|
70
|
+
}
|
|
71
|
+
return { active: true, type, detail: null, invalidReason: null };
|
|
72
|
+
}
|
|
73
|
+
async function readJson(path) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(path, 'utf8');
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve hook config from layered sources, in priority order:
|
|
84
|
+
*
|
|
85
|
+
* 1. Env vars (MNEMONIK_API_KEY, MNEMONIK_SERVER) — let users override per-shell.
|
|
86
|
+
* 2. ~/.mnemonik/scanner.json — already the source of truth for the scanner
|
|
87
|
+
* daemon, so reusing it means users with the scanner working are wired
|
|
88
|
+
* for hooks for free.
|
|
89
|
+
* 3. <cwd>/.mnemonik.json — for the per-project `forcingFunctions` block.
|
|
90
|
+
*
|
|
91
|
+
* Missing API key returns `apiKey: null` rather than throwing — the dispatcher
|
|
92
|
+
* fail-opens with a stderr warning so a misconfigured machine never breaks
|
|
93
|
+
* the user's workflow.
|
|
94
|
+
*/
|
|
95
|
+
export async function resolveConfig(cwd, env = process.env) {
|
|
96
|
+
const scanner = await readJson(join(homedir(), '.mnemonik', 'scanner.json'));
|
|
97
|
+
const projectConfig = await readJson(join(cwd, '.mnemonik.json'));
|
|
98
|
+
const apiKey = env.MNEMONIK_API_KEY?.trim() || scanner?.apiKey?.trim() || null;
|
|
99
|
+
const server = (env.MNEMONIK_SERVER?.trim() || scanner?.server?.trim() || DEFAULT_SERVER).replace(/\/$/, '');
|
|
100
|
+
const forcingFunctions = {
|
|
101
|
+
...DEFAULT_FORCING_FUNCTIONS,
|
|
102
|
+
...(projectConfig?.forcingFunctions ?? {}),
|
|
103
|
+
};
|
|
104
|
+
return { apiKey, server, forcingFunctions };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/lib/env.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny HTTP client for the hook → Mnemonik server path. Hard 2-second budget;
|
|
3
|
+
* any timeout / network error returns null so the dispatcher fails open.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node 20's global `fetch` — zero npm dependencies on the runtime side.
|
|
6
|
+
*/
|
|
7
|
+
import type { SnapshotResponse } from './types.js';
|
|
8
|
+
export interface FetchSnapshotInput {
|
|
9
|
+
server: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
claudeSessionId: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function fetchSnapshot(input: FetchSnapshotInput): Promise<SnapshotResponse | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Fire-and-forget bypass telemetry. We always write to a local audit log and
|
|
17
|
+
* additionally try to POST it to the server, but neither failure is allowed
|
|
18
|
+
* to delay or block the hook decision.
|
|
19
|
+
*/
|
|
20
|
+
export interface ReportBypassInput {
|
|
21
|
+
server: string;
|
|
22
|
+
apiKey: string | null;
|
|
23
|
+
cwd: string;
|
|
24
|
+
claudeSessionId: string;
|
|
25
|
+
tool: string;
|
|
26
|
+
bypassType: string;
|
|
27
|
+
detail: string | null;
|
|
28
|
+
}
|
|
29
|
+
export declare function reportBypass(input: ReportBypassInput): Promise<void>;
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny HTTP client for the hook → Mnemonik server path. Hard 2-second budget;
|
|
3
|
+
* any timeout / network error returns null so the dispatcher fails open.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node 20's global `fetch` — zero npm dependencies on the runtime side.
|
|
6
|
+
*/
|
|
7
|
+
const TIMEOUT_MS = 2000;
|
|
8
|
+
export async function fetchSnapshot(input) {
|
|
9
|
+
const url = `${input.server.replace(/\/$/, '')}/api/v1/hooks/snapshot`;
|
|
10
|
+
const ac = new AbortController();
|
|
11
|
+
const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(url, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'content-type': 'application/json',
|
|
17
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({ cwd: input.cwd, claudeSessionId: input.claudeSessionId }),
|
|
20
|
+
signal: ac.signal,
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
return null;
|
|
24
|
+
return (await res.json());
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function reportBypass(input) {
|
|
34
|
+
if (!input.apiKey)
|
|
35
|
+
return;
|
|
36
|
+
const url = `${input.server.replace(/\/$/, '')}/api/v1/hooks/bypass`;
|
|
37
|
+
const ac = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
39
|
+
try {
|
|
40
|
+
await fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'content-type': 'application/json',
|
|
44
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
cwd: input.cwd,
|
|
48
|
+
claudeSessionId: input.claudeSessionId,
|
|
49
|
+
tool: input.tool,
|
|
50
|
+
bypassType: input.bypassType,
|
|
51
|
+
detail: input.detail,
|
|
52
|
+
}),
|
|
53
|
+
signal: ac.signal,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Telemetry must not crash the hook.
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,UAAU,GAAG,IAAI,CAAC;AASxB,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAyB;IAC3D,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC;IACvE,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,KAAK,CAAC,MAAM,EAAE;aACxC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,CAAC,eAAe,EAAE,CAAC;YAChF,MAAM,EAAE,EAAE,CAAC,MAAM;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QACzB,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqB,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAiBD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAwB;IACzD,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO;IAC1B,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC;IACrE,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,EAAE;YACf,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,KAAK,CAAC,MAAM,EAAE;aACxC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,eAAe,EAAE,KAAK,CAAC,eAAe;gBACtC,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;aACrB,CAAC;YACF,MAAM,EAAE,EAAE,CAAC,MAAM;SAClB,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;IACvC,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny shell tokenizer good enough for matching destructive Bash patterns.
|
|
3
|
+
*
|
|
4
|
+
* Why not a full shell parser? Because:
|
|
5
|
+
* - We only need to recognise patterns like `git push origin main` or
|
|
6
|
+
* `rm -rf /`, not interpret pipes, redirects, command substitution, or
|
|
7
|
+
* shell builtins.
|
|
8
|
+
* - A full parser introduces a runtime dep — the dispatcher is meant to
|
|
9
|
+
* be a single self-contained file with zero npm dependencies.
|
|
10
|
+
* - The regex-only approach is the false-positive trap the plan explicitly
|
|
11
|
+
* calls out (e.g. matching `rm -rf` inside a string literal). We avoid
|
|
12
|
+
* that by tokenising first and only matching tokens, never substrings.
|
|
13
|
+
*
|
|
14
|
+
* Limitations the caller MUST know:
|
|
15
|
+
* - Single-quoted strings are kept verbatim as one token (correct).
|
|
16
|
+
* - Double-quoted strings keep their content but $expansion is NOT resolved.
|
|
17
|
+
* - Backticks / $(...) are tokenised as raw text, not recursed into.
|
|
18
|
+
* - Pipes / redirects / && / || split commands so each side can be matched
|
|
19
|
+
* independently — the result is a list of token arrays, one per command.
|
|
20
|
+
*/
|
|
21
|
+
export interface TokenizeResult {
|
|
22
|
+
/** Each entry is one command in a pipeline / sequence. */
|
|
23
|
+
commands: string[][];
|
|
24
|
+
}
|
|
25
|
+
export declare function tokenize(input: string): TokenizeResult;
|
|
26
|
+
/**
|
|
27
|
+
* Match a token sequence against a project policy pattern.
|
|
28
|
+
*
|
|
29
|
+
* Patterns can be:
|
|
30
|
+
* - Plain phrases like "git push origin main" — substring-matched against
|
|
31
|
+
* the joined command after tokenisation. Whitespace-normalised both sides.
|
|
32
|
+
* - Glob-ish wildcards like "git push *" → "*" matches any single token.
|
|
33
|
+
* - Regex: when the pattern starts with "/" and ends with "/" or "/i",
|
|
34
|
+
* the inner expression is compiled and run against the joined command.
|
|
35
|
+
*
|
|
36
|
+
* Returning the matched pattern (verbatim) lets the dispatcher key bypass
|
|
37
|
+
* telemetry / dedup against `recentPolicyChecks` exactly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function matchCommand(commandTokens: string[], patterns: string[]): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Built-in destructive patterns. Project-level policies extend this set;
|
|
42
|
+
* they're never shrunk, so a project can only ADD restrictions to what's
|
|
43
|
+
* here, never remove them.
|
|
44
|
+
*
|
|
45
|
+
* Kept intentionally narrow — only commands where the cost of asking is
|
|
46
|
+
* small and the cost of NOT asking is large.
|
|
47
|
+
*/
|
|
48
|
+
export declare const BUILT_IN_DESTRUCTIVE: readonly string[];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny shell tokenizer good enough for matching destructive Bash patterns.
|
|
3
|
+
*
|
|
4
|
+
* Why not a full shell parser? Because:
|
|
5
|
+
* - We only need to recognise patterns like `git push origin main` or
|
|
6
|
+
* `rm -rf /`, not interpret pipes, redirects, command substitution, or
|
|
7
|
+
* shell builtins.
|
|
8
|
+
* - A full parser introduces a runtime dep — the dispatcher is meant to
|
|
9
|
+
* be a single self-contained file with zero npm dependencies.
|
|
10
|
+
* - The regex-only approach is the false-positive trap the plan explicitly
|
|
11
|
+
* calls out (e.g. matching `rm -rf` inside a string literal). We avoid
|
|
12
|
+
* that by tokenising first and only matching tokens, never substrings.
|
|
13
|
+
*
|
|
14
|
+
* Limitations the caller MUST know:
|
|
15
|
+
* - Single-quoted strings are kept verbatim as one token (correct).
|
|
16
|
+
* - Double-quoted strings keep their content but $expansion is NOT resolved.
|
|
17
|
+
* - Backticks / $(...) are tokenised as raw text, not recursed into.
|
|
18
|
+
* - Pipes / redirects / && / || split commands so each side can be matched
|
|
19
|
+
* independently — the result is a list of token arrays, one per command.
|
|
20
|
+
*/
|
|
21
|
+
const META_TOKENS = new Set(['|', '||', '&&', ';', '&', '>', '>>', '<', '<<']);
|
|
22
|
+
export function tokenize(input) {
|
|
23
|
+
const commands = [];
|
|
24
|
+
let current = [];
|
|
25
|
+
let buf = '';
|
|
26
|
+
let mode = 'normal';
|
|
27
|
+
const flushToken = () => {
|
|
28
|
+
if (buf.length > 0) {
|
|
29
|
+
current.push(buf);
|
|
30
|
+
buf = '';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const flushCommand = () => {
|
|
34
|
+
flushToken();
|
|
35
|
+
if (current.length > 0)
|
|
36
|
+
commands.push(current);
|
|
37
|
+
current = [];
|
|
38
|
+
};
|
|
39
|
+
for (let i = 0; i < input.length; i++) {
|
|
40
|
+
const c = input[i];
|
|
41
|
+
if (mode === 'single') {
|
|
42
|
+
if (c === "'") {
|
|
43
|
+
mode = 'normal';
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
buf += c;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (mode === 'double') {
|
|
51
|
+
if (c === '"') {
|
|
52
|
+
mode = 'normal';
|
|
53
|
+
}
|
|
54
|
+
else if (c === '\\' && i + 1 < input.length) {
|
|
55
|
+
// Handle escaped quotes inside double-quoted strings.
|
|
56
|
+
buf += input[++i];
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
buf += c;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// mode === 'normal'
|
|
64
|
+
if (c === "'") {
|
|
65
|
+
mode = 'single';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (c === '"') {
|
|
69
|
+
mode = 'double';
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (c === '\\' && i + 1 < input.length) {
|
|
73
|
+
buf += input[++i];
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === ' ' || c === '\t' || c === '\n') {
|
|
77
|
+
flushToken();
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Two-char meta tokens we recognise.
|
|
81
|
+
if ((c === '|' && input[i + 1] === '|') ||
|
|
82
|
+
(c === '&' && input[i + 1] === '&') ||
|
|
83
|
+
(c === '>' && input[i + 1] === '>') ||
|
|
84
|
+
(c === '<' && input[i + 1] === '<')) {
|
|
85
|
+
flushCommand();
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c === '|' || c === ';' || c === '&') {
|
|
90
|
+
flushCommand();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === '>' || c === '<') {
|
|
94
|
+
flushCommand();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
buf += c;
|
|
98
|
+
}
|
|
99
|
+
flushCommand();
|
|
100
|
+
return { commands };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Match a token sequence against a project policy pattern.
|
|
104
|
+
*
|
|
105
|
+
* Patterns can be:
|
|
106
|
+
* - Plain phrases like "git push origin main" — substring-matched against
|
|
107
|
+
* the joined command after tokenisation. Whitespace-normalised both sides.
|
|
108
|
+
* - Glob-ish wildcards like "git push *" → "*" matches any single token.
|
|
109
|
+
* - Regex: when the pattern starts with "/" and ends with "/" or "/i",
|
|
110
|
+
* the inner expression is compiled and run against the joined command.
|
|
111
|
+
*
|
|
112
|
+
* Returning the matched pattern (verbatim) lets the dispatcher key bypass
|
|
113
|
+
* telemetry / dedup against `recentPolicyChecks` exactly.
|
|
114
|
+
*/
|
|
115
|
+
export function matchCommand(commandTokens, patterns) {
|
|
116
|
+
if (commandTokens.length === 0 || patterns.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const joined = commandTokens.join(' ').trim();
|
|
119
|
+
for (const raw of patterns) {
|
|
120
|
+
const pattern = raw.trim();
|
|
121
|
+
if (!pattern)
|
|
122
|
+
continue;
|
|
123
|
+
if (pattern.startsWith('/')) {
|
|
124
|
+
// /regex/ or /regex/i
|
|
125
|
+
const tail = pattern.endsWith('/i') ? '/i' : pattern.endsWith('/') ? '/' : null;
|
|
126
|
+
if (!tail)
|
|
127
|
+
continue;
|
|
128
|
+
const body = pattern.slice(1, pattern.length - tail.length);
|
|
129
|
+
try {
|
|
130
|
+
const re = new RegExp(body, tail === '/i' ? 'i' : '');
|
|
131
|
+
if (re.test(joined))
|
|
132
|
+
return pattern;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Invalid regex pattern — skip rather than crash.
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (pattern.includes('*')) {
|
|
140
|
+
const escapedSegments = pattern
|
|
141
|
+
.split('*')
|
|
142
|
+
.map((seg) => seg.replace(/[.+?^${}()|[\]\\]/g, '\\$&'));
|
|
143
|
+
const re = new RegExp(`^${escapedSegments.join('.*')}$`);
|
|
144
|
+
if (re.test(joined))
|
|
145
|
+
return pattern;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Plain substring — common case for forbiddenCommand patterns like
|
|
149
|
+
// "git push origin main".
|
|
150
|
+
if (joined.includes(pattern))
|
|
151
|
+
return pattern;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Built-in destructive patterns. Project-level policies extend this set;
|
|
157
|
+
* they're never shrunk, so a project can only ADD restrictions to what's
|
|
158
|
+
* here, never remove them.
|
|
159
|
+
*
|
|
160
|
+
* Kept intentionally narrow — only commands where the cost of asking is
|
|
161
|
+
* small and the cost of NOT asking is large.
|
|
162
|
+
*/
|
|
163
|
+
export const BUILT_IN_DESTRUCTIVE = [
|
|
164
|
+
'git push origin main',
|
|
165
|
+
'git push origin master',
|
|
166
|
+
'git push --force',
|
|
167
|
+
'git reset --hard',
|
|
168
|
+
'git merge main',
|
|
169
|
+
'git merge master',
|
|
170
|
+
'git branch -D',
|
|
171
|
+
'npm publish',
|
|
172
|
+
'pnpm publish',
|
|
173
|
+
'yarn publish',
|
|
174
|
+
'docker rm',
|
|
175
|
+
'docker rmi',
|
|
176
|
+
'kubectl delete',
|
|
177
|
+
'rm -rf /',
|
|
178
|
+
'rm -rf ~',
|
|
179
|
+
'rm -rf $HOME',
|
|
180
|
+
];
|
|
181
|
+
//# sourceMappingURL=shellTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellTokens.js","sourceRoot":"","sources":["../../src/lib/shellTokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code hook protocol — only the fields this dispatcher actually reads.
|
|
3
|
+
* Spec: https://docs.claude.com/en/docs/claude-code/hooks
|
|
4
|
+
*
|
|
5
|
+
* We intentionally stay narrow: extra fields the host may ship are ignored
|
|
6
|
+
* silently so newer Claude Code versions don't break the dispatcher.
|
|
7
|
+
*/
|
|
8
|
+
export type HookEventName = 'PreToolUse' | 'PostToolUse' | 'SessionStart' | 'Stop' | 'UserPromptSubmit' | 'Notification' | 'PreCompact' | 'SessionEnd';
|
|
9
|
+
export interface HookInput {
|
|
10
|
+
session_id: string;
|
|
11
|
+
transcript_path?: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
hook_event_name: HookEventName;
|
|
14
|
+
permission_mode?: string;
|
|
15
|
+
tool_name?: string;
|
|
16
|
+
tool_input?: Record<string, unknown>;
|
|
17
|
+
tool_response?: Record<string, unknown>;
|
|
18
|
+
tool_use_id?: string;
|
|
19
|
+
}
|
|
20
|
+
/** PreToolUse decision shape recognised by Claude Code. */
|
|
21
|
+
export type PermissionDecision = 'allow' | 'deny' | 'ask' | 'defer';
|
|
22
|
+
export interface PreToolUseDecision {
|
|
23
|
+
hookSpecificOutput: {
|
|
24
|
+
hookEventName: 'PreToolUse';
|
|
25
|
+
permissionDecision?: PermissionDecision;
|
|
26
|
+
permissionDecisionReason?: string;
|
|
27
|
+
additionalContext?: string;
|
|
28
|
+
updatedInput?: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface PostToolUseDecision {
|
|
32
|
+
hookSpecificOutput: {
|
|
33
|
+
hookEventName: 'PostToolUse';
|
|
34
|
+
additionalContext?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Bypass envelope wired through MNEMONIK_BYPASS_HOOKS=1. */
|
|
38
|
+
export type BypassType = 'typo' | 'formatting' | 'regenerated' | 'already_grounded' | 'other';
|
|
39
|
+
export interface BypassEnvelope {
|
|
40
|
+
active: boolean;
|
|
41
|
+
type: BypassType | null;
|
|
42
|
+
detail: string | null;
|
|
43
|
+
/** When non-null, the bypass envelope was malformed and the gate must NOT
|
|
44
|
+
* honour it — we'd rather fail open silently than silently let a typo
|
|
45
|
+
* in MNEMONIK_BYPASS_TYPE turn into a permanent escape hatch. */
|
|
46
|
+
invalidReason: string | null;
|
|
47
|
+
}
|
|
48
|
+
/** Server snapshot returned by /api/v1/hooks/snapshot. */
|
|
49
|
+
export interface SnapshotResponse {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
reason?: 'unauthorized' | 'no_project' | 'no_active_session';
|
|
52
|
+
coveredFiles: string[];
|
|
53
|
+
editedFiles: string[];
|
|
54
|
+
mustConfirmPauseAt: number | null;
|
|
55
|
+
recentPolicyChecks: Record<string, number>;
|
|
56
|
+
policies: {
|
|
57
|
+
forbiddenCommand: Array<{
|
|
58
|
+
pattern: string;
|
|
59
|
+
description: string;
|
|
60
|
+
}>;
|
|
61
|
+
mustConfirm: Array<{
|
|
62
|
+
pattern: string;
|
|
63
|
+
description: string;
|
|
64
|
+
replacement: string | null;
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
sessionsMerged: number;
|
|
68
|
+
now: number;
|
|
69
|
+
}
|
|
70
|
+
/** Minimal config read from .mnemonik.json + ~/.mnemonik/scanner.json. */
|
|
71
|
+
export interface HookConfig {
|
|
72
|
+
apiKey: string | null;
|
|
73
|
+
server: string;
|
|
74
|
+
forcingFunctions: {
|
|
75
|
+
preEditGate: 'advisory' | 'enforce' | 'off';
|
|
76
|
+
preBashGate: 'advisory' | 'enforce' | 'off';
|
|
77
|
+
advisoryAutoFetch: boolean;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export declare const DEFAULT_FORCING_FUNCTIONS: HookConfig['forcingFunctions'];
|
|
81
|
+
export declare const DEFAULT_SERVER = "https://api.mnemonik.dev";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code hook protocol — only the fields this dispatcher actually reads.
|
|
3
|
+
* Spec: https://docs.claude.com/en/docs/claude-code/hooks
|
|
4
|
+
*
|
|
5
|
+
* We intentionally stay narrow: extra fields the host may ship are ignored
|
|
6
|
+
* silently so newer Claude Code versions don't break the dispatcher.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_FORCING_FUNCTIONS = {
|
|
9
|
+
// Advisory at first ship — the plan only flips enforce by default at v4.0
|
|
10
|
+
// if dogfood telemetry shows bypass rate <5% and false-positive rate <1%.
|
|
11
|
+
preEditGate: 'advisory',
|
|
12
|
+
preBashGate: 'advisory',
|
|
13
|
+
advisoryAutoFetch: true,
|
|
14
|
+
};
|
|
15
|
+
export const DEFAULT_SERVER = 'https://api.mnemonik.dev';
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwFH,MAAM,CAAC,MAAM,yBAAyB,GAAmC;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,WAAW,EAAE,UAAU;IACvB,WAAW,EAAE,UAAU;IACvB,iBAAiB,EAAE,IAAI;CACxB,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mnemonik/claude-code-hooks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code PreToolUse / PostToolUse hooks for Mnemonik forcing-functions (advisory grounding gate). filesToWrite delivery of skill/rules/.mnemonik.json continues to flow for ALL clients including Claude Code — this package adds host-side hook gating only. Versioned independently from the @mnemonik/server release cadence (mirrors @mnemonik/scanner).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mnemonik-claude-code-hooks": "dist/install.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/install.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mnemonik",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"hooks"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.3.3",
|
|
28
|
+
"@types/node": "^22.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|