@davesheffer/hunch 1.32.3 → 1.32.5
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/README.md +1 -1
- package/dist/cli/index.js +40 -7
- package/dist/cli/taskReport.js +80 -1
- package/dist/constitution/behaviorEvaluator.js +1 -1
- package/dist/constitution/repository.d.ts +1 -0
- package/dist/constitution/repository.js +68 -42
- package/dist/core/agenthook.d.ts +1 -1
- package/dist/core/agenthook.js +30 -6
- package/dist/core/checkreport.js +1 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.js +4 -1
- package/dist/core/events.js +19 -3
- package/dist/core/io.js +30 -19
- package/dist/core/jsonc.js +13 -3
- package/dist/core/storeArtifact.d.ts +7 -0
- package/dist/core/storeArtifact.js +62 -0
- package/dist/core/taskReport.d.ts +49 -0
- package/dist/core/taskReport.js +99 -0
- package/dist/core/taskReportHook.d.ts +7 -1
- package/dist/core/taskReportHook.js +18 -5
- package/dist/integrations/claudeConfig.js +20 -3
- package/dist/integrations/claudemd.js +1 -1
- package/dist/integrations/health.d.ts +19 -5
- package/dist/integrations/health.js +36 -11
- package/dist/integrations/providers.d.ts +7 -0
- package/dist/integrations/providers.js +85 -16
- package/dist/integrations/registry.d.ts +15 -0
- package/dist/integrations/registry.js +41 -0
- package/dist/integrations/scaffold.js +17 -3
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +350 -324
- package/dist/mcp/toolset.d.ts +30 -0
- package/dist/mcp/toolset.js +72 -0
- package/dist/serve/app.js +9 -6
- package/dist/serve/writelock.js +8 -2
- package/dist/store/changeLedger.js +7 -6
- package/dist/store/jsonStore.d.ts +3 -3
- package/dist/store/jsonStore.js +65 -14
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -49,6 +49,13 @@ export declare function writeWindsurfRule(root: string, store: HunchStore): stri
|
|
|
49
49
|
* registration remain the durable grounding path if a Cursor build suppresses
|
|
50
50
|
* a hook's agent_message. */
|
|
51
51
|
export declare function writeCursorHooks(root: string, inv: Invocation): string;
|
|
52
|
+
/** Codex CLI hooks (0.153+): `.codex/hooks.json`, the same event names, stdin
|
|
53
|
+
* payload, and stdout contract as Claude Code, with `turn_id` as the per-prompt
|
|
54
|
+
* identity and `apply_patch` as the edit tool. Project-layer hooks load only for
|
|
55
|
+
* a trusted project and must be trusted once in Codex (`/hooks`), which the
|
|
56
|
+
* scaffold output says. Codex's PreToolUse rejects `continue:false`, so the
|
|
57
|
+
* strict gate answers with `permissionDecision` — the shape Hunch already emits. */
|
|
58
|
+
export declare function writeCodexHooks(root: string, inv: Invocation): string;
|
|
52
59
|
/** VS Code's native workspace hook location. It supports all lifecycle events
|
|
53
60
|
* Hunch needs and uses the same stdout contract as Claude Code, with different
|
|
54
61
|
* camelCase tool fields normalized in core/agenthook.ts. */
|
|
@@ -24,6 +24,7 @@ import { join, dirname } from "node:path";
|
|
|
24
24
|
import { renderHunchSection, stripManagedSection, upsertSection, updateClaudeMd } from "./claudemd.js";
|
|
25
25
|
import { headFileContent, isGitCleanPath } from "../extractors/git.js";
|
|
26
26
|
import { parseJsonc } from "../core/jsonc.js";
|
|
27
|
+
import { parse as parseToml } from "smol-toml";
|
|
27
28
|
/** Read a JSON/JSONC object. Returns {} only for an ABSENT or empty file. A
|
|
28
29
|
* non-empty file we cannot parse THROWS — overwriting it would silently wipe the
|
|
29
30
|
* user's other MCP servers. */
|
|
@@ -65,6 +66,18 @@ function writeJson(file, obj) {
|
|
|
65
66
|
writeFileAtomic(file, JSON.stringify(obj, null, 2) + "\n");
|
|
66
67
|
return file;
|
|
67
68
|
}
|
|
69
|
+
/** A present managed container must have the object shape its host expects.
|
|
70
|
+
* Arrays and primitives are valid JSON/TOML values, but assigning properties to
|
|
71
|
+
* them either throws or silently disappears during JSON serialization. Refuse
|
|
72
|
+
* those shapes instead of claiming an install succeeded. */
|
|
73
|
+
function objectField(json, key, file) {
|
|
74
|
+
const value = json[key];
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return {};
|
|
77
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
78
|
+
return value;
|
|
79
|
+
throw new Error(`refusing to edit ${file}: ${key} must be a JSON object when present; fix it, then re-run.`);
|
|
80
|
+
}
|
|
68
81
|
/** Quote one argv token only when it needs quoting. These commands are run by
|
|
69
82
|
* whatever shell the host assistant uses, which on Windows is PowerShell — and
|
|
70
83
|
* PowerShell parses a QUOTED first token as a string expression, not a command,
|
|
@@ -89,6 +102,10 @@ function hookCommand(inv, provider) {
|
|
|
89
102
|
}
|
|
90
103
|
function isHunchProviderHook(entry) {
|
|
91
104
|
const e = entry && typeof entry === "object" ? entry : null;
|
|
105
|
+
// Codex (like Claude Code) nests commands under a matcher entry: an entry
|
|
106
|
+
// whose every nested hook is ours is ours; a mixed entry stays foreign.
|
|
107
|
+
if (e && Array.isArray(e.hooks) && e.hooks.length && typeof e.command !== "string")
|
|
108
|
+
return e.hooks.every(isHunchProviderHook);
|
|
92
109
|
const command = typeof e?.command === "string" ? e.command : "";
|
|
93
110
|
// Anchored to the shapes hookCommand() writes — a Hunch launcher (the pinned
|
|
94
111
|
// npm package spec, or a …/index.js|ts path for source installs) plus a tail
|
|
@@ -99,7 +116,10 @@ function isHunchProviderHook(entry) {
|
|
|
99
116
|
// the bare tail must still carry --provider to match; only the LEGACY
|
|
100
117
|
// fully-quoted form (written before this quoting fix, and by hunch versions
|
|
101
118
|
// that predate --provider) may omit it, and its quotes keep it unambiguous.
|
|
102
|
-
|
|
119
|
+
// Source-checkout invocations generated by resolveInvocation point at the
|
|
120
|
+
// CLI entry specifically. A generic `other-tool/index.js hook` is foreign
|
|
121
|
+
// even when it happens to use Hunch's old command tail.
|
|
122
|
+
const launcher = /@davesheffer\/hunch|(?:dist|src)[\\/]+cli[\\/]+index\.(?:js|ts)(?=["\s]|$)/.test(command);
|
|
103
123
|
const legacyTail = /\s"hook"(?:\s+"--provider"\s+"[a-z]+")?\s*$/.test(command);
|
|
104
124
|
const tail = /\s"?hook"?\s+"?--provider"?\s+"?[a-z]+"?\s*$/.test(command);
|
|
105
125
|
return launcher && (legacyTail || tail);
|
|
@@ -108,11 +128,13 @@ function isHunchProviderHook(entry) {
|
|
|
108
128
|
* We replace only old Hunch commands and leave every foreign hook in place. */
|
|
109
129
|
function writeHookConfig(file, entries) {
|
|
110
130
|
const json = readJsonObj(file);
|
|
111
|
-
const hooks = json
|
|
112
|
-
? json.hooks
|
|
113
|
-
: {};
|
|
131
|
+
const hooks = objectField(json, "hooks", file);
|
|
114
132
|
for (const [event, next] of Object.entries(entries)) {
|
|
115
|
-
const
|
|
133
|
+
const existing = hooks[event];
|
|
134
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
135
|
+
throw new Error(`refusing to edit ${file}: hooks.${event} must be an array when present; fix it, then re-run.`);
|
|
136
|
+
}
|
|
137
|
+
const old = (existing ?? []);
|
|
116
138
|
hooks[event] = [...old.filter((entry) => !isHunchProviderHook(entry)), ...next];
|
|
117
139
|
}
|
|
118
140
|
json.hooks = hooks;
|
|
@@ -122,7 +144,7 @@ function writeHookConfig(file, entries) {
|
|
|
122
144
|
export function writeCursorMcp(root, inv) {
|
|
123
145
|
const file = join(root, ".cursor", "mcp.json");
|
|
124
146
|
const json = readJsonObj(file);
|
|
125
|
-
json.mcpServers = json
|
|
147
|
+
json.mcpServers = objectField(json, "mcpServers", file);
|
|
126
148
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
127
149
|
return writeJson(file, json);
|
|
128
150
|
}
|
|
@@ -131,7 +153,7 @@ export function writeCursorMcp(root, inv) {
|
|
|
131
153
|
export function writeVscodeMcp(root, inv) {
|
|
132
154
|
const file = join(root, ".vscode", "mcp.json");
|
|
133
155
|
const json = readJsonObj(file);
|
|
134
|
-
json.servers = json
|
|
156
|
+
json.servers = objectField(json, "servers", file);
|
|
135
157
|
json.servers.hunch = { type: "stdio", command: inv.command, args: [...inv.args, "mcp"] };
|
|
136
158
|
return writeJson(file, json);
|
|
137
159
|
}
|
|
@@ -162,7 +184,7 @@ export function writeAntigravityMcp(inv, home = homedir()) {
|
|
|
162
184
|
if (!file)
|
|
163
185
|
return null;
|
|
164
186
|
const json = readJsonObj(file);
|
|
165
|
-
json.mcpServers = json
|
|
187
|
+
json.mcpServers = objectField(json, "mcpServers", file);
|
|
166
188
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
167
189
|
return writeJson(file, json);
|
|
168
190
|
}
|
|
@@ -172,7 +194,7 @@ export function writeAntigravityMcp(inv, home = homedir()) {
|
|
|
172
194
|
export function writeAntigravityWorkspaceMcp(root, inv) {
|
|
173
195
|
const file = join(root, ".agents", "mcp_config.json");
|
|
174
196
|
const json = readJsonObj(file);
|
|
175
|
-
json.mcpServers = json
|
|
197
|
+
json.mcpServers = objectField(json, "mcpServers", file);
|
|
176
198
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
177
199
|
return writeJson(file, json);
|
|
178
200
|
}
|
|
@@ -189,6 +211,8 @@ export function writeCodexConfig(root, inv) {
|
|
|
189
211
|
"[mcp_servers.hunch]",
|
|
190
212
|
`command = ${tomlStr(inv.command)}`,
|
|
191
213
|
`args = [${argsToml}]`,
|
|
214
|
+
"# A cold npx install can exceed Codex's 10 s default; a slow start must not drop Hunch from the tool catalog.",
|
|
215
|
+
"startup_timeout_sec = 60",
|
|
192
216
|
TOML_END,
|
|
193
217
|
].join("\n");
|
|
194
218
|
// Strip any prior managed block first, so `base` is the user's own TOML.
|
|
@@ -207,9 +231,30 @@ export function writeCodexConfig(root, inv) {
|
|
|
207
231
|
if (/^\s*\[mcp_servers\.hunch\]/m.test(base)) {
|
|
208
232
|
throw new Error(`refusing to edit ${file}: it already defines [mcp_servers.hunch] outside Hunch's managed block. Remove it, then re-run.`);
|
|
209
233
|
}
|
|
234
|
+
// Validate the complete original document before stripping any managed block.
|
|
235
|
+
// Even Hunch-owned malformed content must be preserved until the user reviews
|
|
236
|
+
// it; otherwise a repair can silently erase an unparseable configuration.
|
|
237
|
+
if (content.trim()) {
|
|
238
|
+
try {
|
|
239
|
+
parseToml(content);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
throw new Error(`refusing to overwrite ${file}: could not parse TOML (${e.message}). Fix it, then re-run.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
210
245
|
base = base.trimEnd();
|
|
246
|
+
const next = base ? `${base}\n\n${block}\n` : `${block}\n`;
|
|
247
|
+
// A syntactically valid parent value can still make the appended table
|
|
248
|
+
// illegal (`mcp_servers = 42` followed by `[mcp_servers.hunch]`). Validate
|
|
249
|
+
// the exact candidate before touching the user's file.
|
|
250
|
+
try {
|
|
251
|
+
parseToml(next);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
throw new Error(`refusing to overwrite ${file}: resulting TOML is invalid (${e.message}). Fix it, then re-run.`);
|
|
255
|
+
}
|
|
211
256
|
mkdirSync(dirname(file), { recursive: true });
|
|
212
|
-
writeFileAtomic(file,
|
|
257
|
+
writeFileAtomic(file, next);
|
|
213
258
|
return file;
|
|
214
259
|
}
|
|
215
260
|
/** AGENTS.md — the cross-tool ambient-instruction standard (Codex and a growing
|
|
@@ -236,7 +281,7 @@ export function writeCursorRule(root, store) {
|
|
|
236
281
|
export function writeWindsurfMcp(root, inv) {
|
|
237
282
|
const file = join(root, ".windsurf", "mcp_config.json");
|
|
238
283
|
const json = readJsonObj(file);
|
|
239
|
-
json.mcpServers = json
|
|
284
|
+
json.mcpServers = objectField(json, "mcpServers", file);
|
|
240
285
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
241
286
|
return writeJson(file, json);
|
|
242
287
|
}
|
|
@@ -252,7 +297,7 @@ export function writeWindsurfGlobalMcp(inv, home = homedir()) {
|
|
|
252
297
|
if (!file)
|
|
253
298
|
return null;
|
|
254
299
|
const json = readJsonObj(file);
|
|
255
|
-
json.mcpServers = json
|
|
300
|
+
json.mcpServers = objectField(json, "mcpServers", file);
|
|
256
301
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
257
302
|
return writeJson(file, json);
|
|
258
303
|
}
|
|
@@ -286,6 +331,26 @@ export function writeCursorHooks(root, inv) {
|
|
|
286
331
|
}
|
|
287
332
|
return written;
|
|
288
333
|
}
|
|
334
|
+
/** Codex CLI hooks (0.153+): `.codex/hooks.json`, the same event names, stdin
|
|
335
|
+
* payload, and stdout contract as Claude Code, with `turn_id` as the per-prompt
|
|
336
|
+
* identity and `apply_patch` as the edit tool. Project-layer hooks load only for
|
|
337
|
+
* a trusted project and must be trusted once in Codex (`/hooks`), which the
|
|
338
|
+
* scaffold output says. Codex's PreToolUse rejects `continue:false`, so the
|
|
339
|
+
* strict gate answers with `permissionDecision` — the shape Hunch already emits. */
|
|
340
|
+
export function writeCodexHooks(root, inv) {
|
|
341
|
+
const file = join(root, ".codex", "hooks.json");
|
|
342
|
+
const command = hookCommand(inv, "codex");
|
|
343
|
+
const entry = (matcher) => ({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command }] });
|
|
344
|
+
return writeHookConfig(file, {
|
|
345
|
+
SessionStart: [entry()],
|
|
346
|
+
UserPromptSubmit: [entry()],
|
|
347
|
+
PreToolUse: [entry("apply_patch")],
|
|
348
|
+
PostToolUse: [entry("apply_patch|shell|local_shell")],
|
|
349
|
+
Stop: [entry()],
|
|
350
|
+
PreCompact: [entry()],
|
|
351
|
+
SubagentStart: [entry()],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
289
354
|
/** VS Code's native workspace hook location. It supports all lifecycle events
|
|
290
355
|
* Hunch needs and uses the same stdout contract as Claude Code, with different
|
|
291
356
|
* camelCase tool fields normalized in core/agenthook.ts. */
|
|
@@ -321,9 +386,13 @@ function antigravityHandler(command) {
|
|
|
321
386
|
export function writeAntigravityHooks(root, inv) {
|
|
322
387
|
const file = join(root, ".agents", "hooks.json");
|
|
323
388
|
const json = readJsonObj(file);
|
|
324
|
-
const group = json
|
|
325
|
-
|
|
326
|
-
|
|
389
|
+
const group = objectField(json, "hunch", file);
|
|
390
|
+
for (const event of ["PreInvocation", "PreToolUse", "Stop"]) {
|
|
391
|
+
const existing = group[event];
|
|
392
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
393
|
+
throw new Error(`refusing to edit ${file}: hunch.${event} must be an array when present; fix it, then re-run.`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
327
396
|
const command = hookCommand(inv, "antigravity");
|
|
328
397
|
const keep = (event) => Array.isArray(group[event])
|
|
329
398
|
? group[event].filter((entry) => {
|
|
@@ -470,7 +539,7 @@ export function scaffoldProviders(root, inv, store, options = {}) {
|
|
|
470
539
|
const tasks = [
|
|
471
540
|
["Cursor", () => runProvider([() => writeCursorMcp(root, inv), () => writeCursorRule(root, store), ...(hooks ? [() => writeCursorHooks(root, inv)] : [])])],
|
|
472
541
|
["VS Code (Copilot)", () => runProvider([() => writeVscodeMcp(root, inv), () => writeCopilotInstructions(root, store), ...(hooks ? [() => writeVscodeHooks(root, inv)] : [])])],
|
|
473
|
-
["Codex CLI", () => runProvider([() => writeCodexConfig(root, inv)])],
|
|
542
|
+
["Codex CLI", () => runProvider([() => writeCodexConfig(root, inv), ...(hooks ? [() => writeCodexHooks(root, inv)] : [])])],
|
|
474
543
|
["Windsurf", () => {
|
|
475
544
|
return runProvider([
|
|
476
545
|
() => writeWindsurfMcp(root, inv),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type PublishedStatus = "published" | "unpublished" | "unknown";
|
|
2
|
+
export interface NpmResult {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
error?: Error;
|
|
7
|
+
}
|
|
8
|
+
export type NpmRunner = (args: string[]) => NpmResult;
|
|
9
|
+
export declare function defaultNpmRunner(timeoutMs: number): NpmRunner;
|
|
10
|
+
/** Whether npm can serve `@davesheffer/hunch@<version>`. "unknown" covers offline,
|
|
11
|
+
* timeouts, and unexpected output; callers must never read it as unpublished. */
|
|
12
|
+
export declare function publishedStatus(version: string, opts?: {
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
run?: NpmRunner;
|
|
15
|
+
}): PublishedStatus;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Bounded npm registry lookups that never throw. A pin npm cannot serve makes
|
|
2
|
+
* every `npx --package=…` launcher (hooks and MCP) fail before Hunch runs, and
|
|
3
|
+
* the hosts report nothing — so the CLI must be able to name that state. */
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
const exactVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6
|
+
export function defaultNpmRunner(timeoutMs) {
|
|
7
|
+
return (args) => {
|
|
8
|
+
const windows = process.platform === "win32";
|
|
9
|
+
const r = spawnSync(windows ? `npm ${args.join(" ")}` : "npm", windows ? [] : args, {
|
|
10
|
+
shell: windows, windowsHide: true, encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"],
|
|
11
|
+
});
|
|
12
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Whether npm can serve `@davesheffer/hunch@<version>`. "unknown" covers offline,
|
|
16
|
+
* timeouts, and unexpected output; callers must never read it as unpublished. */
|
|
17
|
+
export function publishedStatus(version, opts = {}) {
|
|
18
|
+
if (!exactVersion.test(version))
|
|
19
|
+
return "unknown";
|
|
20
|
+
const run = opts.run ?? defaultNpmRunner(opts.timeoutMs ?? 8000);
|
|
21
|
+
let r;
|
|
22
|
+
try {
|
|
23
|
+
r = run(["view", `@davesheffer/hunch@${version}`, "version", "--json"]);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
if (r.error)
|
|
29
|
+
return "unknown";
|
|
30
|
+
if (r.status === 0) {
|
|
31
|
+
try {
|
|
32
|
+
const v = JSON.parse(r.stdout.trim() || "null");
|
|
33
|
+
return v === version || (Array.isArray(v) && v.includes(version)) ? "published" : "unknown";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return /\b(?:ETARGET|E404|notarget)\b/.test(r.stderr) ? "unpublished" : "unknown";
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -27,7 +27,11 @@ export function writeMcpJson(root, inv) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const servers = json.mcpServers;
|
|
31
|
+
if (servers !== undefined && (!servers || typeof servers !== "object" || Array.isArray(servers))) {
|
|
32
|
+
throw new Error(`refusing to edit ${file}: mcpServers must be a JSON object when present; fix it, then re-run.`);
|
|
33
|
+
}
|
|
34
|
+
json.mcpServers = servers ?? {};
|
|
31
35
|
json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
|
|
32
36
|
// Atomic: .mcp.json holds the user's other servers — a torn write would leave
|
|
33
37
|
// it unparseable, which this writer then refuses to touch (issue #43).
|
|
@@ -114,7 +118,7 @@ function isHunchHook(entry) {
|
|
|
114
118
|
if (typeof h.command !== "string")
|
|
115
119
|
return false;
|
|
116
120
|
const command = h.command;
|
|
117
|
-
const nativeOrSource = /[\\/]index\.(js|ts)"?\s+hook\s*$/.test(command);
|
|
121
|
+
const nativeOrSource = /(?:dist|src)[\\/]+cli[\\/]+index\.(js|ts)"?\s+hook\s*$/.test(command);
|
|
118
122
|
const publishedNpx = /^\s*"?npx(?:\.cmd)?"?\s+/i.test(command)
|
|
119
123
|
&& /--package=(?:hunch-exact@npm:)?@davesheffer\/hunch(?:@[^"\s]+)?/.test(command)
|
|
120
124
|
&& /\s"?hunch"?\s+"?hook"?\s*$/.test(command);
|
|
@@ -151,7 +155,17 @@ export function installClaudeHooks(root, hookCmd) {
|
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
|
-
|
|
158
|
+
const hooks = json.hooks;
|
|
159
|
+
if (hooks !== undefined && (!hooks || typeof hooks !== "object" || Array.isArray(hooks))) {
|
|
160
|
+
throw new Error(`refusing to edit ${file}: hooks must be a JSON object when present; fix it, then re-run.`);
|
|
161
|
+
}
|
|
162
|
+
json.hooks = hooks ?? {};
|
|
163
|
+
for (const event of ["PreToolUse", "UserPromptSubmit", "SessionStart", "SubagentStart", "PreCompact", "PostToolUse", "PostToolUseFailure", "Stop"]) {
|
|
164
|
+
const existing = json.hooks[event];
|
|
165
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
166
|
+
throw new Error(`refusing to edit ${file}: hooks.${event} must be an array when present; fix it, then re-run.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
155
169
|
const keep = (arr) => (Array.isArray(arr) ? arr.filter((e) => !isHunchHook(e)) : []);
|
|
156
170
|
json.hooks.PreToolUse = [
|
|
157
171
|
...keep(json.hooks.PreToolUse),
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface RootControlOptions {
|
|
|
25
25
|
/** Serve exactly `initialRoot`; never re-home to client roots or `cwd` hints. */
|
|
26
26
|
pinned?: boolean;
|
|
27
27
|
}
|
|
28
|
+
/** Delivered to every MCP client at initialize — the one grounding channel that
|
|
29
|
+
* needs no host hook or instruction file. Host-neutral by design (con_e04226bd05);
|
|
30
|
+
* per-host prose (CLAUDE.md, AGENTS.md) and hooks add to it, never replace it. */
|
|
31
|
+
export declare const MCP_INSTRUCTIONS: string;
|
|
28
32
|
export declare function buildServerWithRootControl(initialRoot: string, options?: RootControlOptions): RootControlledServer;
|
|
29
33
|
/** Back-compatible server construction for tests and callers that do not need
|
|
30
34
|
* to drive roots directly. The server still owns and closes its active store. */
|