@hasna/hooks 0.2.5 → 0.2.6
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/bin/index.js +215 -7
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -4183,13 +4183,20 @@ function createHooksServer() {
|
|
|
4183
4183
|
name: "@hasna/hooks",
|
|
4184
4184
|
version: pkg.version
|
|
4185
4185
|
});
|
|
4186
|
-
server.tool("hooks_list", "List all available hooks, optionally filtered by category
|
|
4186
|
+
server.tool("hooks_list", "List all available hooks, optionally filtered by category. Use compact:true to get minimal output (name+event+matcher only) \u2014 saves tokens.", {
|
|
4187
|
+
category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
|
|
4188
|
+
compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher. Reduces token usage.")
|
|
4189
|
+
}, async ({ category, compact }) => {
|
|
4190
|
+
const slim = (hooks) => compact ? hooks.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : hooks;
|
|
4187
4191
|
if (category) {
|
|
4188
4192
|
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
4189
4193
|
if (!cat) {
|
|
4190
4194
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
|
|
4191
4195
|
}
|
|
4192
|
-
return { content: [{ type: "text", text: JSON.stringify(getHooksByCategory(cat)) }] };
|
|
4196
|
+
return { content: [{ type: "text", text: JSON.stringify(slim(getHooksByCategory(cat))) }] };
|
|
4197
|
+
}
|
|
4198
|
+
if (compact) {
|
|
4199
|
+
return { content: [{ type: "text", text: JSON.stringify(slim(HOOKS)) }] };
|
|
4193
4200
|
}
|
|
4194
4201
|
const result = {};
|
|
4195
4202
|
for (const cat of CATEGORIES) {
|
|
@@ -4197,9 +4204,13 @@ function createHooksServer() {
|
|
|
4197
4204
|
}
|
|
4198
4205
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4199
4206
|
});
|
|
4200
|
-
server.tool("hooks_search", "Search for hooks by name, description, or tags
|
|
4207
|
+
server.tool("hooks_search", "Search for hooks by name, description, or tags. Use compact:true for minimal output to save tokens.", {
|
|
4208
|
+
query: z.string().describe("Search query"),
|
|
4209
|
+
compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher.")
|
|
4210
|
+
}, async ({ query, compact }) => {
|
|
4201
4211
|
const results = searchHooks(query);
|
|
4202
|
-
|
|
4212
|
+
const out = compact ? results.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : results;
|
|
4213
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
4203
4214
|
});
|
|
4204
4215
|
server.tool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
|
|
4205
4216
|
const meta = getHook(name);
|
|
@@ -4286,7 +4297,7 @@ function createHooksServer() {
|
|
|
4286
4297
|
if (hookHealthy)
|
|
4287
4298
|
healthy.push(name);
|
|
4288
4299
|
}
|
|
4289
|
-
return { content: [{ type: "text", text: JSON.stringify({ healthy, issues, registered, scope }) }] };
|
|
4300
|
+
return { content: [{ type: "text", text: JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }) }] };
|
|
4290
4301
|
});
|
|
4291
4302
|
server.tool("hooks_categories", "List all hook categories with counts", {}, async () => {
|
|
4292
4303
|
const result = CATEGORIES.map((cat) => ({
|
|
@@ -4339,7 +4350,7 @@ function createHooksServer() {
|
|
|
4339
4350
|
const registered = getRegisteredHooks(scope);
|
|
4340
4351
|
const result = registered.map((name) => {
|
|
4341
4352
|
const meta = getHook(name);
|
|
4342
|
-
return { name, event: meta?.event, version: meta?.version, description: meta?.description };
|
|
4353
|
+
return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version, description: meta?.description };
|
|
4343
4354
|
});
|
|
4344
4355
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4345
4356
|
});
|
|
@@ -4426,6 +4437,203 @@ function createHooksServer() {
|
|
|
4426
4437
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
4427
4438
|
return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
|
|
4428
4439
|
});
|
|
4440
|
+
server.tool("hooks_context", "Get full agent context in one call: installed hooks (with event+matcher), active profile, settings path, and doctor status. Call this once at session start instead of making 4 separate calls.", {
|
|
4441
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to inspect"),
|
|
4442
|
+
profile: z.string().optional().describe("Agent profile ID to include in context")
|
|
4443
|
+
}, async ({ scope, profile }) => {
|
|
4444
|
+
const settingsPath = getSettingsPath(scope);
|
|
4445
|
+
const registered = getRegisteredHooks(scope);
|
|
4446
|
+
const hooks = registered.map((name) => {
|
|
4447
|
+
const meta = getHook(name);
|
|
4448
|
+
return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version };
|
|
4449
|
+
});
|
|
4450
|
+
const issues = [];
|
|
4451
|
+
for (const name of registered) {
|
|
4452
|
+
if (!hookExists(name)) {
|
|
4453
|
+
issues.push({ hook: name, issue: "Hook not found in package", severity: "error" });
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
const healthy = issues.length === 0;
|
|
4457
|
+
const ctx = {
|
|
4458
|
+
scope,
|
|
4459
|
+
settings_path: settingsPath,
|
|
4460
|
+
settings_exists: existsSync3(settingsPath),
|
|
4461
|
+
registered_hooks: hooks,
|
|
4462
|
+
hook_count: hooks.length,
|
|
4463
|
+
healthy,
|
|
4464
|
+
issues,
|
|
4465
|
+
version: pkg.version
|
|
4466
|
+
};
|
|
4467
|
+
if (profile) {
|
|
4468
|
+
const p = getProfile(profile);
|
|
4469
|
+
ctx.profile = p ?? null;
|
|
4470
|
+
}
|
|
4471
|
+
return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
|
|
4472
|
+
});
|
|
4473
|
+
server.tool("hooks_preview", "Simulate which installed PreToolUse hooks would fire for a given tool call and what decision each returns. Use this to understand your hook environment before taking an action.", {
|
|
4474
|
+
tool_name: z.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
4475
|
+
tool_input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
4476
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
4477
|
+
timeout_ms: z.number().default(5000).describe("Per-hook timeout in milliseconds")
|
|
4478
|
+
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
4479
|
+
const registered = getRegisteredHooks(scope);
|
|
4480
|
+
const matchingHooks = registered.filter((name) => {
|
|
4481
|
+
const meta = getHook(name);
|
|
4482
|
+
if (!meta || meta.event !== "PreToolUse")
|
|
4483
|
+
return false;
|
|
4484
|
+
if (!meta.matcher)
|
|
4485
|
+
return true;
|
|
4486
|
+
try {
|
|
4487
|
+
return new RegExp(meta.matcher).test(tool_name);
|
|
4488
|
+
} catch {
|
|
4489
|
+
return false;
|
|
4490
|
+
}
|
|
4491
|
+
});
|
|
4492
|
+
if (matchingHooks.length === 0) {
|
|
4493
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool_name, matching_hooks: [], result: "no_hooks_match", decision: "approve" }) }] };
|
|
4494
|
+
}
|
|
4495
|
+
const input = { tool_name, tool_input };
|
|
4496
|
+
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
4497
|
+
const hookDir = getHookPath(name);
|
|
4498
|
+
const hookScript = join3(hookDir, "src", "hook.ts");
|
|
4499
|
+
if (!existsSync3(hookScript))
|
|
4500
|
+
return { name, decision: "approve", error: "script not found" };
|
|
4501
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4502
|
+
stdin: new Response(JSON.stringify(input)),
|
|
4503
|
+
stdout: "pipe",
|
|
4504
|
+
stderr: "pipe",
|
|
4505
|
+
env: process.env
|
|
4506
|
+
});
|
|
4507
|
+
const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
|
|
4508
|
+
const res = await Promise.race([
|
|
4509
|
+
Promise.all([new Response(proc.stdout).text(), proc.exited]).then(([stdout]) => ({ stdout, timedOut: false })),
|
|
4510
|
+
timeout.then(() => {
|
|
4511
|
+
proc.kill();
|
|
4512
|
+
return { stdout: "", timedOut: true };
|
|
4513
|
+
})
|
|
4514
|
+
]);
|
|
4515
|
+
if (res.timedOut)
|
|
4516
|
+
return { name, decision: "approve", timedOut: true };
|
|
4517
|
+
let output = {};
|
|
4518
|
+
try {
|
|
4519
|
+
output = JSON.parse(res.stdout);
|
|
4520
|
+
} catch {}
|
|
4521
|
+
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
4522
|
+
}));
|
|
4523
|
+
const blocked = results.find((r) => r.decision === "block");
|
|
4524
|
+
return {
|
|
4525
|
+
content: [{
|
|
4526
|
+
type: "text",
|
|
4527
|
+
text: JSON.stringify({
|
|
4528
|
+
tool_name,
|
|
4529
|
+
matching_hooks: matchingHooks,
|
|
4530
|
+
results,
|
|
4531
|
+
decision: blocked ? "block" : "approve",
|
|
4532
|
+
blocked_by: blocked?.name ?? null,
|
|
4533
|
+
blocked_reason: blocked?.reason ?? null
|
|
4534
|
+
})
|
|
4535
|
+
}]
|
|
4536
|
+
};
|
|
4537
|
+
});
|
|
4538
|
+
server.tool("hooks_setup", "Single-shot agent onboarding: create an agent profile + install recommended hooks in one call. Ideal for agents setting up hooks at session start.", {
|
|
4539
|
+
agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
4540
|
+
name: z.string().optional().describe("Optional display name for the agent"),
|
|
4541
|
+
hooks: z.array(z.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
4542
|
+
scope: z.enum(["global", "project"]).default("global").describe("Install scope")
|
|
4543
|
+
}, async ({ agent_type, name, hooks, scope }) => {
|
|
4544
|
+
const profile = createProfile({ agent_type, name });
|
|
4545
|
+
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
4546
|
+
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
|
|
4547
|
+
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
4548
|
+
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
4549
|
+
return {
|
|
4550
|
+
content: [{
|
|
4551
|
+
type: "text",
|
|
4552
|
+
text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
4553
|
+
}]
|
|
4554
|
+
};
|
|
4555
|
+
});
|
|
4556
|
+
server.tool("hooks_batch_run", "Run multiple hooks in parallel in a single call. Returns all results at once \u2014 more efficient than N separate hooks_run calls.", {
|
|
4557
|
+
hooks: z.array(z.object({
|
|
4558
|
+
name: z.string().describe("Hook name"),
|
|
4559
|
+
input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
4560
|
+
})).describe("List of hooks to run with their inputs"),
|
|
4561
|
+
timeout_ms: z.number().default(1e4).describe("Per-hook timeout in milliseconds")
|
|
4562
|
+
}, async ({ hooks, timeout_ms }) => {
|
|
4563
|
+
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
4564
|
+
const meta = getHook(name);
|
|
4565
|
+
if (!meta)
|
|
4566
|
+
return { name, error: `Hook '${name}' not found` };
|
|
4567
|
+
const hookScript = join3(getHookPath(name), "src", "hook.ts");
|
|
4568
|
+
if (!existsSync3(hookScript))
|
|
4569
|
+
return { name, error: "script not found" };
|
|
4570
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
4571
|
+
stdin: new Response(JSON.stringify(input)),
|
|
4572
|
+
stdout: "pipe",
|
|
4573
|
+
stderr: "pipe",
|
|
4574
|
+
env: process.env
|
|
4575
|
+
});
|
|
4576
|
+
const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
|
|
4577
|
+
const res = await Promise.race([
|
|
4578
|
+
Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
|
|
4579
|
+
timeout.then(() => {
|
|
4580
|
+
proc.kill();
|
|
4581
|
+
return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
|
|
4582
|
+
})
|
|
4583
|
+
]);
|
|
4584
|
+
let output = {};
|
|
4585
|
+
try {
|
|
4586
|
+
output = JSON.parse(res.stdout);
|
|
4587
|
+
} catch {
|
|
4588
|
+
output = res.stdout ? { raw: res.stdout } : {};
|
|
4589
|
+
}
|
|
4590
|
+
return { name, output, exitCode: res.exitCode, ...res.timedOut ? { timedOut: true } : {} };
|
|
4591
|
+
}));
|
|
4592
|
+
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
4593
|
+
});
|
|
4594
|
+
server.tool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
|
|
4595
|
+
name: z.string().describe("Hook name to disable"),
|
|
4596
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope")
|
|
4597
|
+
}, async ({ name, scope }) => {
|
|
4598
|
+
const settingsPath = getSettingsPath(scope);
|
|
4599
|
+
let settings = {};
|
|
4600
|
+
try {
|
|
4601
|
+
if (existsSync3(settingsPath))
|
|
4602
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
4603
|
+
} catch {}
|
|
4604
|
+
if (!settings.hooks)
|
|
4605
|
+
settings.hooks = {};
|
|
4606
|
+
const disabled = settings.hooks.__disabled ?? [];
|
|
4607
|
+
if (!disabled.includes(name))
|
|
4608
|
+
disabled.push(name);
|
|
4609
|
+
settings.hooks.__disabled = disabled;
|
|
4610
|
+
const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
|
|
4611
|
+
const { dirname: dirname3 } = await import("path");
|
|
4612
|
+
mkdirSync3(dirname3(settingsPath), { recursive: true });
|
|
4613
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4614
|
+
`);
|
|
4615
|
+
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
4616
|
+
});
|
|
4617
|
+
server.tool("hooks_enable", "Re-enable a previously disabled hook.", {
|
|
4618
|
+
name: z.string().describe("Hook name to enable"),
|
|
4619
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope")
|
|
4620
|
+
}, async ({ name, scope }) => {
|
|
4621
|
+
const settingsPath = getSettingsPath(scope);
|
|
4622
|
+
let settings = {};
|
|
4623
|
+
try {
|
|
4624
|
+
if (existsSync3(settingsPath))
|
|
4625
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
4626
|
+
} catch {}
|
|
4627
|
+
if (settings.hooks?.__disabled) {
|
|
4628
|
+
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
4629
|
+
if (settings.hooks.__disabled.length === 0)
|
|
4630
|
+
delete settings.hooks.__disabled;
|
|
4631
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
4632
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4633
|
+
`);
|
|
4634
|
+
}
|
|
4635
|
+
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
4636
|
+
});
|
|
4429
4637
|
server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
|
|
4430
4638
|
agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
4431
4639
|
name: z.string().optional().describe("Optional display name for the agent")
|
|
@@ -6095,7 +6303,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
6095
6303
|
}
|
|
6096
6304
|
}
|
|
6097
6305
|
if (options.json) {
|
|
6098
|
-
console.log(JSON.stringify({ healthy, issues, registered, scope }));
|
|
6306
|
+
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
|
|
6099
6307
|
return;
|
|
6100
6308
|
}
|
|
6101
6309
|
console.log(chalk2.bold(`
|
package/package.json
CHANGED