@hasna/hooks 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +302 -107
  2. package/bin/index.js +316 -51
  3. package/dist/index.js +216 -14
  4. package/hooks/hook-autoformat/LICENSE +191 -0
  5. package/hooks/hook-autostage/LICENSE +191 -0
  6. package/hooks/hook-branchprotect/LICENSE +191 -0
  7. package/hooks/hook-checkdocs/LICENSE +191 -0
  8. package/hooks/hook-checkpoint/LICENSE +191 -0
  9. package/hooks/hook-checktasks/LICENSE +191 -0
  10. package/hooks/hook-commandlog/LICENSE +191 -0
  11. package/hooks/hook-contextrefresh/LICENSE +191 -0
  12. package/hooks/hook-costwatch/LICENSE +191 -0
  13. package/hooks/hook-desktopnotify/LICENSE +191 -0
  14. package/hooks/hook-envsetup/LICENSE +191 -0
  15. package/hooks/hook-errornotify/LICENSE +191 -0
  16. package/hooks/hook-gitguard/LICENSE +191 -0
  17. package/hooks/hook-packageage/LICENSE +191 -0
  18. package/hooks/hook-permissionguard/LICENSE +191 -0
  19. package/hooks/hook-phonenotify/LICENSE +191 -0
  20. package/hooks/hook-precompact/LICENSE +191 -0
  21. package/hooks/hook-promptguard/LICENSE +191 -0
  22. package/hooks/hook-protectfiles/LICENSE +191 -0
  23. package/hooks/hook-sessionlog/LICENSE +191 -0
  24. package/hooks/hook-slacknotify/LICENSE +191 -0
  25. package/hooks/hook-soundnotify/LICENSE +191 -0
  26. package/hooks/hook-stylescheck/LICENSE +191 -0
  27. package/hooks/hook-stylescheck/README.md +41 -0
  28. package/hooks/hook-stylescheck/package.json +52 -0
  29. package/hooks/hook-stylescheck/src/hook.ts +213 -0
  30. package/hooks/hook-stylescheck/tsconfig.json +25 -0
  31. package/hooks/hook-taskgate/LICENSE +191 -0
  32. package/hooks/hook-tddguard/LICENSE +191 -0
  33. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -2184,6 +2184,16 @@ var init_registry = __esm(() => {
2184
2184
  matcher: "",
2185
2185
  tags: ["errors", "failures", "logging", "debugging"]
2186
2186
  },
2187
+ {
2188
+ name: "stylescheck",
2189
+ displayName: "Styles Check",
2190
+ description: "Blocks frontend files with design anti-patterns: hardcoded colors, magic font sizes, inline styles",
2191
+ version: "0.1.0",
2192
+ category: "Code Quality",
2193
+ event: "PreToolUse",
2194
+ matcher: "Write|Edit",
2195
+ tags: ["design", "styles", "frontend", "css", "tailwind", "design-system", "anti-patterns"]
2196
+ },
2187
2197
  {
2188
2198
  name: "taskgate",
2189
2199
  displayName: "Task Gate",
@@ -3848,8 +3858,11 @@ function normalizeHookName(name) {
3848
3858
  function shortHookName(name) {
3849
3859
  return normalizeHookName(name).replace("hook-", "");
3850
3860
  }
3851
- function removeHookEntries(entries, hookCommand) {
3852
- return entries.filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
3861
+ function removeHookEntriesByName(entries, hookName) {
3862
+ return entries.filter((entry) => !entry.hooks?.some((h) => {
3863
+ const match = h.command?.match(/^hooks run (\w+)/);
3864
+ return match && match[1] === hookName;
3865
+ }));
3853
3866
  }
3854
3867
  function getTargetSettingsDir(target) {
3855
3868
  if (target === "gemini")
@@ -3892,7 +3905,7 @@ function writeSettings(settings, scope = "global", target = "claude") {
3892
3905
  function getTargetEventName(internalEvent, target) {
3893
3906
  return EVENT_MAP[target]?.[internalEvent] || internalEvent;
3894
3907
  }
3895
- function installForTarget(name, scope, overwrite, target) {
3908
+ function installForTarget(name, scope, overwrite, target, profile) {
3896
3909
  const shortName = shortHookName(name);
3897
3910
  if (!hookExists(shortName)) {
3898
3911
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
@@ -3902,7 +3915,7 @@ function installForTarget(name, scope, overwrite, target) {
3902
3915
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
3903
3916
  }
3904
3917
  try {
3905
- registerHook(shortName, scope, target);
3918
+ registerHook(shortName, scope, target, profile);
3906
3919
  return { hook: shortName, success: true, scope, target };
3907
3920
  } catch (error) {
3908
3921
  return {
@@ -3914,15 +3927,15 @@ function installForTarget(name, scope, overwrite, target) {
3914
3927
  }
3915
3928
  }
3916
3929
  function installHook(name, options = {}) {
3917
- const { scope = "global", overwrite = false, target = "claude" } = options;
3930
+ const { scope = "global", overwrite = false, target = "claude", profile } = options;
3918
3931
  if (target === "all") {
3919
- const claudeResult = installForTarget(name, scope, overwrite, "claude");
3920
- installForTarget(name, scope, overwrite, "gemini");
3932
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
3933
+ installForTarget(name, scope, overwrite, "gemini", profile);
3921
3934
  return { ...claudeResult, target: "all" };
3922
3935
  }
3923
- return installForTarget(name, scope, overwrite, target);
3936
+ return installForTarget(name, scope, overwrite, target, profile);
3924
3937
  }
3925
- function registerHook(name, scope = "global", target = "claude") {
3938
+ function registerHook(name, scope = "global", target = "claude", profile) {
3926
3939
  const meta = getHook(name);
3927
3940
  if (!meta)
3928
3941
  return;
@@ -3932,8 +3945,8 @@ function registerHook(name, scope = "global", target = "claude") {
3932
3945
  const eventKey = getTargetEventName(meta.event, target);
3933
3946
  if (!settings.hooks[eventKey])
3934
3947
  settings.hooks[eventKey] = [];
3935
- const hookCommand = `hooks run ${name}`;
3936
- settings.hooks[eventKey] = removeHookEntries(settings.hooks[eventKey], hookCommand);
3948
+ settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
3949
+ const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
3937
3950
  const entry = {
3938
3951
  hooks: [{ type: "command", command: hookCommand }]
3939
3952
  };
@@ -3953,8 +3966,7 @@ function unregisterHook(name, scope = "global", target = "claude") {
3953
3966
  const eventKey = getTargetEventName(meta.event, target);
3954
3967
  if (!settings.hooks[eventKey])
3955
3968
  return;
3956
- const hookCommand = `hooks run ${name}`;
3957
- settings.hooks[eventKey] = removeHookEntries(settings.hooks[eventKey], hookCommand);
3969
+ settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
3958
3970
  if (settings.hooks[eventKey].length === 0) {
3959
3971
  delete settings.hooks[eventKey];
3960
3972
  }
@@ -3971,7 +3983,7 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3971
3983
  for (const eventKey of Object.keys(settings.hooks)) {
3972
3984
  for (const entry of settings.hooks[eventKey]) {
3973
3985
  for (const hook of entry.hooks || []) {
3974
- const newMatch = hook.command?.match(/^hooks run (\w+)$/);
3986
+ const newMatch = hook.command?.match(/^hooks run (\w+)(?:\s+--profile\s+\w+)?$/);
3975
3987
  const oldMatch = hook.command?.match(/^hook-(\w+)$/);
3976
3988
  const match = newMatch || oldMatch;
3977
3989
  if (match) {
@@ -4024,6 +4036,79 @@ var init_installer = __esm(() => {
4024
4036
  getInstalledHooks = getRegisteredHooks;
4025
4037
  });
4026
4038
 
4039
+ // src/lib/profiles.ts
4040
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync } from "fs";
4041
+ import { join as join2 } from "path";
4042
+ import { homedir as homedir2 } from "os";
4043
+ function ensureProfilesDir() {
4044
+ if (!existsSync2(PROFILES_DIR)) {
4045
+ mkdirSync2(PROFILES_DIR, { recursive: true });
4046
+ }
4047
+ }
4048
+ function profilePath(id) {
4049
+ return join2(PROFILES_DIR, `${id}.json`);
4050
+ }
4051
+ function shortUuid() {
4052
+ return crypto.randomUUID().slice(0, 8);
4053
+ }
4054
+ function createProfile(input) {
4055
+ ensureProfilesDir();
4056
+ const id = shortUuid();
4057
+ const now = new Date().toISOString();
4058
+ const profile = {
4059
+ agent_id: id,
4060
+ agent_type: input.agent_type,
4061
+ created_at: now,
4062
+ last_seen_at: now,
4063
+ preferences: {}
4064
+ };
4065
+ if (input.name) {
4066
+ profile.name = input.name;
4067
+ }
4068
+ writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4069
+ `);
4070
+ return profile;
4071
+ }
4072
+ function getProfile(id) {
4073
+ const path = profilePath(id);
4074
+ try {
4075
+ if (!existsSync2(path))
4076
+ return null;
4077
+ return JSON.parse(readFileSync2(path, "utf-8"));
4078
+ } catch {
4079
+ return null;
4080
+ }
4081
+ }
4082
+ function listProfiles() {
4083
+ if (!existsSync2(PROFILES_DIR))
4084
+ return [];
4085
+ try {
4086
+ const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4087
+ const profiles = [];
4088
+ for (const file of files) {
4089
+ try {
4090
+ const content = readFileSync2(join2(PROFILES_DIR, file), "utf-8");
4091
+ profiles.push(JSON.parse(content));
4092
+ } catch {}
4093
+ }
4094
+ return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
4095
+ } catch {
4096
+ return [];
4097
+ }
4098
+ }
4099
+ function touchProfile(id) {
4100
+ const profile = getProfile(id);
4101
+ if (!profile)
4102
+ return;
4103
+ profile.last_seen_at = new Date().toISOString();
4104
+ writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4105
+ `);
4106
+ }
4107
+ var PROFILES_DIR;
4108
+ var init_profiles = __esm(() => {
4109
+ PROFILES_DIR = join2(homedir2(), ".hooks", "profiles");
4110
+ });
4111
+
4027
4112
  // src/mcp/server.ts
4028
4113
  var exports_server = {};
4029
4114
  __export(exports_server, {
@@ -4037,8 +4122,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4037
4122
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4038
4123
  import { z } from "zod";
4039
4124
  import { createServer } from "http";
4040
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4041
- import { join as join2, dirname as dirname2 } from "path";
4125
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4126
+ import { join as join3, dirname as dirname2 } from "path";
4042
4127
  import { fileURLToPath as fileURLToPath2 } from "url";
4043
4128
  function formatInstallResults(results, extra) {
4044
4129
  const installed = results.filter((r) => r.success).map((r) => r.hook);
@@ -4085,10 +4170,11 @@ function createHooksServer() {
4085
4170
  server.tool("hooks_install", "Install one or more hooks by registering them in agent settings", {
4086
4171
  hooks: z.array(z.string()).describe("Hook names to install"),
4087
4172
  scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
4088
- overwrite: z.boolean().default(false).describe("Overwrite if already installed")
4089
- }, async ({ hooks, scope, overwrite }) => {
4090
- const results = hooks.map((name) => installHook(name, { scope, overwrite }));
4091
- return formatInstallResults(results, { scope });
4173
+ overwrite: z.boolean().default(false).describe("Overwrite if already installed"),
4174
+ profile: z.string().optional().describe("Agent profile ID to scope hooks to")
4175
+ }, async ({ hooks, scope, overwrite, profile }) => {
4176
+ const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
4177
+ return formatInstallResults(results, { scope, profile });
4092
4178
  });
4093
4179
  server.tool("hooks_install_category", "Install all hooks in a category", {
4094
4180
  category: z.string().describe("Category name"),
@@ -4123,7 +4209,7 @@ function createHooksServer() {
4123
4209
  const settingsPath = getSettingsPath(scope);
4124
4210
  const issues = [];
4125
4211
  const healthy = [];
4126
- const settingsExist = existsSync2(settingsPath);
4212
+ const settingsExist = existsSync3(settingsPath);
4127
4213
  if (!settingsExist) {
4128
4214
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4129
4215
  }
@@ -4136,15 +4222,18 @@ function createHooksServer() {
4136
4222
  continue;
4137
4223
  }
4138
4224
  const hookDir = getHookPath(name);
4139
- if (!existsSync2(join2(hookDir, "src", "hook.ts"))) {
4225
+ if (!existsSync3(join3(hookDir, "src", "hook.ts"))) {
4140
4226
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4141
4227
  hookHealthy = false;
4142
4228
  }
4143
4229
  if (meta && settingsExist) {
4144
4230
  try {
4145
- const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
4231
+ const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4146
4232
  const eventHooks = settings.hooks?.[meta.event] || [];
4147
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
4233
+ const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
4234
+ const match = h.command?.match(/^hooks run (\w+)/);
4235
+ return match && match[1] === name;
4236
+ }));
4148
4237
  if (!found) {
4149
4238
  issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
4150
4239
  hookHealthy = false;
@@ -4170,10 +4259,10 @@ function createHooksServer() {
4170
4259
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4171
4260
  }
4172
4261
  const hookPath = getHookPath(name);
4173
- const readmePath = join2(hookPath, "README.md");
4262
+ const readmePath = join3(hookPath, "README.md");
4174
4263
  let readme = "";
4175
- if (existsSync2(readmePath)) {
4176
- readme = readFileSync2(readmePath, "utf-8");
4264
+ if (existsSync3(readmePath)) {
4265
+ readme = readFileSync3(readmePath, "utf-8");
4177
4266
  }
4178
4267
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
4179
4268
  }
@@ -4211,6 +4300,67 @@ function createHooksServer() {
4211
4300
  });
4212
4301
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
4213
4302
  });
4303
+ server.tool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
4304
+ name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
4305
+ input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
4306
+ profile: z.string().optional().describe("Agent profile ID to inject into hook input")
4307
+ }, async ({ name, input, profile }) => {
4308
+ const meta = getHook(name);
4309
+ if (!meta) {
4310
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4311
+ }
4312
+ const hookDir = getHookPath(name);
4313
+ const hookScript = join3(hookDir, "src", "hook.ts");
4314
+ if (!existsSync3(hookScript)) {
4315
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
4316
+ }
4317
+ let hookInput = { ...input };
4318
+ if (profile) {
4319
+ const p = getProfile(profile);
4320
+ if (p) {
4321
+ hookInput.agent = {
4322
+ agent_id: p.agent_id,
4323
+ agent_type: p.agent_type,
4324
+ name: p.name,
4325
+ preferences: p.preferences
4326
+ };
4327
+ }
4328
+ }
4329
+ const proc = Bun.spawn(["bun", "run", hookScript], {
4330
+ stdin: new Response(JSON.stringify(hookInput)),
4331
+ stdout: "pipe",
4332
+ stderr: "pipe",
4333
+ env: process.env
4334
+ });
4335
+ const [stdoutText, stderrText, exitCode] = await Promise.all([
4336
+ new Response(proc.stdout).text(),
4337
+ new Response(proc.stderr).text(),
4338
+ proc.exited
4339
+ ]);
4340
+ let output = {};
4341
+ try {
4342
+ output = JSON.parse(stdoutText);
4343
+ } catch {
4344
+ output = { raw: stdoutText };
4345
+ }
4346
+ return {
4347
+ content: [{
4348
+ type: "text",
4349
+ text: JSON.stringify({ hook: name, output, stderr: stderrText || undefined, exitCode })
4350
+ }]
4351
+ };
4352
+ });
4353
+ server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
4354
+ agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
4355
+ name: z.string().optional().describe("Optional display name for the agent")
4356
+ }, async ({ agent_type, name }) => {
4357
+ const profile = createProfile({ agent_type, name });
4358
+ return { content: [{ type: "text", text: JSON.stringify(profile) }] };
4359
+ });
4360
+ server.tool("hooks_profiles", "List all registered agent profiles", {}, async () => {
4361
+ const profiles = listProfiles();
4362
+ return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
4363
+ });
4214
4364
  return server;
4215
4365
  }
4216
4366
  async function startSSEServer(port = MCP_PORT) {
@@ -4254,8 +4404,9 @@ var __dirname3, pkg, MCP_PORT = 39427;
4254
4404
  var init_server = __esm(() => {
4255
4405
  init_registry();
4256
4406
  init_installer();
4407
+ init_profiles();
4257
4408
  __dirname3 = dirname2(fileURLToPath2(import.meta.url));
4258
- pkg = JSON.parse(readFileSync2(join2(__dirname3, "..", "..", "package.json"), "utf-8"));
4409
+ pkg = JSON.parse(readFileSync3(join3(__dirname3, "..", "..", "package.json"), "utf-8"));
4259
4410
  });
4260
4411
 
4261
4412
  // src/cli/index.tsx
@@ -4279,8 +4430,8 @@ var {
4279
4430
 
4280
4431
  // src/cli/index.tsx
4281
4432
  import chalk2 from "chalk";
4282
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4283
- import { join as join3, dirname as dirname3 } from "path";
4433
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4434
+ import { join as join4, dirname as dirname3 } from "path";
4284
4435
  import { fileURLToPath as fileURLToPath3 } from "url";
4285
4436
 
4286
4437
  // src/cli/components/App.tsx
@@ -5455,10 +5606,11 @@ function App({ initialHooks, overwrite = false }) {
5455
5606
  // src/cli/index.tsx
5456
5607
  init_registry();
5457
5608
  init_installer();
5609
+ init_profiles();
5458
5610
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5459
5611
  var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
5460
- var pkgPath = existsSync3(join3(__dirname4, "..", "package.json")) ? join3(__dirname4, "..", "package.json") : join3(__dirname4, "..", "..", "package.json");
5461
- var pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5612
+ var pkgPath = existsSync4(join4(__dirname4, "..", "package.json")) ? join4(__dirname4, "..", "package.json") : join4(__dirname4, "..", "..", "package.json");
5613
+ var pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
5462
5614
  var program2 = new Command;
5463
5615
  function resolveScope(options) {
5464
5616
  if (options.project)
@@ -5469,21 +5621,68 @@ program2.name("hooks").description("Install hooks for AI coding agents").version
5469
5621
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
5470
5622
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
5471
5623
  });
5472
- program2.command("run").argument("<hook>", "Hook to run").description("Execute a hook (called by AI coding agents)").action(async (hook) => {
5624
+ program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).action((options) => {
5625
+ const agentType = options.agent;
5626
+ if (!["claude", "gemini", "custom"].includes(agentType)) {
5627
+ if (options.json) {
5628
+ console.log(JSON.stringify({ error: `Invalid agent type: ${options.agent}`, valid: ["claude", "gemini", "custom"] }));
5629
+ } else {
5630
+ console.log(chalk2.red(`Invalid agent type: ${options.agent}`));
5631
+ console.log(chalk2.dim("Valid types: claude, gemini, custom"));
5632
+ }
5633
+ return;
5634
+ }
5635
+ const profile = createProfile({ agent_type: agentType, name: options.name });
5636
+ if (options.json) {
5637
+ console.log(JSON.stringify(profile));
5638
+ return;
5639
+ }
5640
+ console.log(chalk2.green(`
5641
+ \u2713 Agent profile created
5642
+ `));
5643
+ console.log(` ${chalk2.dim("Agent ID:")} ${chalk2.bold(profile.agent_id)}`);
5644
+ console.log(` ${chalk2.dim("Type:")} ${profile.agent_type}`);
5645
+ if (profile.name) {
5646
+ console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
5647
+ }
5648
+ console.log(` ${chalk2.dim("Profile:")} ~/.hooks/profiles/${profile.agent_id}.json`);
5649
+ console.log();
5650
+ console.log(chalk2.dim(" Install hooks with this profile:"));
5651
+ console.log(` hooks install gitguard --profile ${profile.agent_id}`);
5652
+ console.log();
5653
+ });
5654
+ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
5473
5655
  const meta = getHook(hook);
5474
5656
  if (!meta) {
5475
5657
  console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
5476
5658
  process.exit(1);
5477
5659
  }
5478
5660
  const hookDir = getHookPath(hook);
5479
- const hookScript = join3(hookDir, "src", "hook.ts");
5480
- if (!existsSync3(hookScript)) {
5661
+ const hookScript = join4(hookDir, "src", "hook.ts");
5662
+ if (!existsSync4(hookScript)) {
5481
5663
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
5482
5664
  process.exit(1);
5483
5665
  }
5484
5666
  const stdin = await new Response(Bun.stdin.stream()).text();
5667
+ let hookStdin = stdin;
5668
+ if (options.profile) {
5669
+ const profile = getProfile(options.profile);
5670
+ if (profile) {
5671
+ touchProfile(options.profile);
5672
+ try {
5673
+ const input = JSON.parse(stdin);
5674
+ input.agent = {
5675
+ agent_id: profile.agent_id,
5676
+ agent_type: profile.agent_type,
5677
+ name: profile.name,
5678
+ preferences: profile.preferences
5679
+ };
5680
+ hookStdin = JSON.stringify(input);
5681
+ } catch {}
5682
+ }
5683
+ }
5485
5684
  const proc = Bun.spawn(["bun", "run", hookScript], {
5486
- stdin: new Response(stdin),
5685
+ stdin: new Response(hookStdin),
5487
5686
  stdout: "pipe",
5488
5687
  stderr: "pipe",
5489
5688
  env: process.env
@@ -5497,7 +5696,7 @@ program2.command("run").argument("<hook>", "Hook to run").description("Execute a
5497
5696
  process.stderr.write(stderr);
5498
5697
  process.exit(exitCode);
5499
5698
  });
5500
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
5699
+ program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("--profile <id>", "Agent profile ID to scope hooks to").option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
5501
5700
  const scope = resolveScope(options);
5502
5701
  let toInstall = hooks;
5503
5702
  if (options.all) {
@@ -5521,7 +5720,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
5521
5720
  }
5522
5721
  const results = [];
5523
5722
  for (const name of toInstall) {
5524
- const result = installHook(name, { scope, overwrite: options.overwrite });
5723
+ const result = installHook(name, { scope, overwrite: options.overwrite, profile: options.profile });
5525
5724
  results.push(result);
5526
5725
  }
5527
5726
  if (options.json) {
@@ -5552,23 +5751,24 @@ Installing hooks (${scope})...
5552
5751
  console.log(chalk2.dim(`
5553
5752
  Registered in ${settingsFile}`));
5554
5753
  });
5555
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
5754
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
5556
5755
  const scope = resolveScope(options);
5557
5756
  if (options.registered || options.installed) {
5558
- const registered = getRegisteredHooks(scope);
5757
+ const target = options.target === "gemini" ? "gemini" : "claude";
5758
+ const registered = getRegisteredHooksForTarget(scope, target);
5559
5759
  if (options.json) {
5560
5760
  console.log(JSON.stringify(registered.map((name) => {
5561
5761
  const meta = getHook(name);
5562
- return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope };
5762
+ return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope, target };
5563
5763
  })));
5564
5764
  return;
5565
5765
  }
5566
5766
  if (registered.length === 0) {
5567
- console.log(chalk2.dim(`No hooks registered (${scope})`));
5767
+ console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
5568
5768
  return;
5569
5769
  }
5570
5770
  console.log(chalk2.bold(`
5571
- Registered hooks \u2014 ${scope} (${registered.length}):
5771
+ Registered hooks \u2014 ${scope}/${target} (${registered.length}):
5572
5772
  `));
5573
5773
  for (const name of registered) {
5574
5774
  const meta = getHook(name);
@@ -5712,7 +5912,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
5712
5912
  const settingsPath = getSettingsPath(scope);
5713
5913
  const issues = [];
5714
5914
  const healthy = [];
5715
- const settingsExist = existsSync3(settingsPath);
5915
+ const settingsExist = existsSync4(settingsPath);
5716
5916
  if (!settingsExist) {
5717
5917
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
5718
5918
  }
@@ -5726,16 +5926,19 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
5726
5926
  continue;
5727
5927
  }
5728
5928
  const hookDir = getHookPath(name);
5729
- const hookScript = join3(hookDir, "src", "hook.ts");
5730
- if (!existsSync3(hookScript)) {
5929
+ const hookScript = join4(hookDir, "src", "hook.ts");
5930
+ if (!existsSync4(hookScript)) {
5731
5931
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
5732
5932
  hookHealthy = false;
5733
5933
  }
5734
5934
  if (meta && settingsExist) {
5735
5935
  try {
5736
- const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
5936
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
5737
5937
  const eventHooks = settings.hooks?.[meta.event] || [];
5738
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
5938
+ const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
5939
+ const match = h.command?.match(/^hooks run (\w+)/);
5940
+ return match && match[1] === name;
5941
+ }));
5739
5942
  if (!found) {
5740
5943
  issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
5741
5944
  hookHealthy = false;
@@ -5828,10 +6031,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
5828
6031
  return;
5829
6032
  }
5830
6033
  const hookPath = getHookPath(hook);
5831
- const readmePath = join3(hookPath, "README.md");
6034
+ const readmePath = join4(hookPath, "README.md");
5832
6035
  let readme = "";
5833
- if (existsSync3(readmePath)) {
5834
- readme = readFileSync3(readmePath, "utf-8");
6036
+ if (existsSync4(readmePath)) {
6037
+ readme = readFileSync4(readmePath, "utf-8");
5835
6038
  }
5836
6039
  if (options.json) {
5837
6040
  console.log(JSON.stringify({ ...meta, readme }));
@@ -5934,6 +6137,68 @@ ${meta.displayName} v${meta.version}
5934
6137
  console.log(` hooks docs --json Machine-readable documentation`);
5935
6138
  console.log();
5936
6139
  });
6140
+ program2.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
6141
+ const current = pkg2.version;
6142
+ let pm = "npm";
6143
+ try {
6144
+ const which = Bun.spawnSync(["which", "bun"]);
6145
+ if (which.exitCode === 0)
6146
+ pm = "bun";
6147
+ } catch {}
6148
+ if (options.check) {
6149
+ const proc2 = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
6150
+ const latest2 = new TextDecoder().decode(proc2.stdout).trim();
6151
+ if (!latest2) {
6152
+ if (options.json) {
6153
+ console.log(JSON.stringify({ error: "Failed to fetch latest version" }));
6154
+ } else {
6155
+ console.log(chalk2.red("Failed to fetch latest version from npm registry."));
6156
+ }
6157
+ process.exit(1);
6158
+ }
6159
+ const upToDate = current === latest2;
6160
+ if (options.json) {
6161
+ console.log(JSON.stringify({ current, latest: latest2, upToDate }));
6162
+ } else if (upToDate) {
6163
+ console.log(chalk2.green(`\u2713 Already on latest version (${current})`));
6164
+ } else {
6165
+ console.log(chalk2.yellow(`Update available: ${current} \u2192 ${latest2}`));
6166
+ console.log(chalk2.dim(` Run: hooks upgrade`));
6167
+ }
6168
+ return;
6169
+ }
6170
+ const installCmd = pm === "bun" ? ["bun", "install", "-g", "@hasna/hooks@latest"] : ["npm", "install", "-g", "@hasna/hooks@latest"];
6171
+ if (!options.json) {
6172
+ console.log(chalk2.bold(`
6173
+ Upgrading @hasna/hooks (${pm})...
6174
+ `));
6175
+ console.log(chalk2.dim(` $ ${installCmd.join(" ")}
6176
+ `));
6177
+ }
6178
+ const proc = Bun.spawn(installCmd, {
6179
+ stdout: options.json ? "pipe" : "inherit",
6180
+ stderr: options.json ? "pipe" : "inherit",
6181
+ env: process.env
6182
+ });
6183
+ const exitCode = await proc.exited;
6184
+ if (exitCode !== 0) {
6185
+ if (options.json) {
6186
+ console.log(JSON.stringify({ current, updated: false, error: `${pm} exited with code ${exitCode}` }));
6187
+ } else {
6188
+ console.log(chalk2.red(`
6189
+ \u2717 Upgrade failed (exit code ${exitCode})`));
6190
+ }
6191
+ process.exit(exitCode);
6192
+ }
6193
+ const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
6194
+ const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
6195
+ if (options.json) {
6196
+ console.log(JSON.stringify({ current, latest, updated: true }));
6197
+ } else {
6198
+ console.log(chalk2.green(`
6199
+ \u2713 Upgraded: ${current} \u2192 ${latest}`));
6200
+ }
6201
+ });
5937
6202
  program2.command("mcp").option("-s, --stdio", "Use stdio transport (for agent MCP integration)", false).option("-p, --port <port>", "Port for SSE transport", "39427").description("Start MCP server for AI agent integration").action(async (options) => {
5938
6203
  if (options.stdio) {
5939
6204
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));