@hasna/hooks 0.2.3 → 0.2.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/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,26 @@ 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 detectConflict(name, scope, target) {
3909
+ const meta = getHook(name);
3910
+ if (!meta || !meta.matcher)
3911
+ return;
3912
+ const registered = getRegisteredHooksForTarget(scope, target);
3913
+ for (const existingName of registered) {
3914
+ if (existingName === name)
3915
+ continue;
3916
+ const existing = getHook(existingName);
3917
+ if (!existing || existing.event !== meta.event || !existing.matcher)
3918
+ continue;
3919
+ const a = meta.matcher.toLowerCase();
3920
+ const b = existing.matcher.toLowerCase();
3921
+ if (a === b || a.includes(b) || b.includes(a)) {
3922
+ return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
3923
+ }
3924
+ }
3925
+ return;
3926
+ }
3927
+ function installForTarget(name, scope, overwrite, target, profile) {
3896
3928
  const shortName = shortHookName(name);
3897
3929
  if (!hookExists(shortName)) {
3898
3930
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
@@ -3901,9 +3933,10 @@ function installForTarget(name, scope, overwrite, target) {
3901
3933
  if (registered.includes(shortName) && !overwrite) {
3902
3934
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
3903
3935
  }
3936
+ const conflict = detectConflict(shortName, scope, target);
3904
3937
  try {
3905
- registerHook(shortName, scope, target);
3906
- return { hook: shortName, success: true, scope, target };
3938
+ registerHook(shortName, scope, target, profile);
3939
+ return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
3907
3940
  } catch (error) {
3908
3941
  return {
3909
3942
  hook: shortName,
@@ -3914,15 +3947,15 @@ function installForTarget(name, scope, overwrite, target) {
3914
3947
  }
3915
3948
  }
3916
3949
  function installHook(name, options = {}) {
3917
- const { scope = "global", overwrite = false, target = "claude" } = options;
3950
+ const { scope = "global", overwrite = false, target = "claude", profile } = options;
3918
3951
  if (target === "all") {
3919
- const claudeResult = installForTarget(name, scope, overwrite, "claude");
3920
- installForTarget(name, scope, overwrite, "gemini");
3952
+ const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
3953
+ installForTarget(name, scope, overwrite, "gemini", profile);
3921
3954
  return { ...claudeResult, target: "all" };
3922
3955
  }
3923
- return installForTarget(name, scope, overwrite, target);
3956
+ return installForTarget(name, scope, overwrite, target, profile);
3924
3957
  }
3925
- function registerHook(name, scope = "global", target = "claude") {
3958
+ function registerHook(name, scope = "global", target = "claude", profile) {
3926
3959
  const meta = getHook(name);
3927
3960
  if (!meta)
3928
3961
  return;
@@ -3932,8 +3965,8 @@ function registerHook(name, scope = "global", target = "claude") {
3932
3965
  const eventKey = getTargetEventName(meta.event, target);
3933
3966
  if (!settings.hooks[eventKey])
3934
3967
  settings.hooks[eventKey] = [];
3935
- const hookCommand = `hooks run ${name}`;
3936
- settings.hooks[eventKey] = removeHookEntries(settings.hooks[eventKey], hookCommand);
3968
+ settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
3969
+ const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
3937
3970
  const entry = {
3938
3971
  hooks: [{ type: "command", command: hookCommand }]
3939
3972
  };
@@ -3953,8 +3986,7 @@ function unregisterHook(name, scope = "global", target = "claude") {
3953
3986
  const eventKey = getTargetEventName(meta.event, target);
3954
3987
  if (!settings.hooks[eventKey])
3955
3988
  return;
3956
- const hookCommand = `hooks run ${name}`;
3957
- settings.hooks[eventKey] = removeHookEntries(settings.hooks[eventKey], hookCommand);
3989
+ settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
3958
3990
  if (settings.hooks[eventKey].length === 0) {
3959
3991
  delete settings.hooks[eventKey];
3960
3992
  }
@@ -3971,7 +4003,7 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3971
4003
  for (const eventKey of Object.keys(settings.hooks)) {
3972
4004
  for (const entry of settings.hooks[eventKey]) {
3973
4005
  for (const hook of entry.hooks || []) {
3974
- const newMatch = hook.command?.match(/^hooks run (\w+)$/);
4006
+ const newMatch = hook.command?.match(/^hooks run (\w+)(?:\s+--profile\s+\w+)?$/);
3975
4007
  const oldMatch = hook.command?.match(/^hook-(\w+)$/);
3976
4008
  const match = newMatch || oldMatch;
3977
4009
  if (match) {
@@ -4024,6 +4056,102 @@ var init_installer = __esm(() => {
4024
4056
  getInstalledHooks = getRegisteredHooks;
4025
4057
  });
4026
4058
 
4059
+ // src/lib/profiles.ts
4060
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync } from "fs";
4061
+ import { join as join2 } from "path";
4062
+ import { homedir as homedir2 } from "os";
4063
+ function ensureProfilesDir() {
4064
+ if (!existsSync2(PROFILES_DIR)) {
4065
+ mkdirSync2(PROFILES_DIR, { recursive: true });
4066
+ }
4067
+ }
4068
+ function profilePath(id) {
4069
+ return join2(PROFILES_DIR, `${id}.json`);
4070
+ }
4071
+ function shortUuid() {
4072
+ return crypto.randomUUID().slice(0, 8);
4073
+ }
4074
+ function createProfile(input) {
4075
+ ensureProfilesDir();
4076
+ const id = shortUuid();
4077
+ const now = new Date().toISOString();
4078
+ const profile = {
4079
+ agent_id: id,
4080
+ agent_type: input.agent_type,
4081
+ created_at: now,
4082
+ last_seen_at: now,
4083
+ preferences: {}
4084
+ };
4085
+ if (input.name) {
4086
+ profile.name = input.name;
4087
+ }
4088
+ writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4089
+ `);
4090
+ return profile;
4091
+ }
4092
+ function getProfile(id) {
4093
+ const path = profilePath(id);
4094
+ try {
4095
+ if (!existsSync2(path))
4096
+ return null;
4097
+ return JSON.parse(readFileSync2(path, "utf-8"));
4098
+ } catch {
4099
+ return null;
4100
+ }
4101
+ }
4102
+ function listProfiles() {
4103
+ if (!existsSync2(PROFILES_DIR))
4104
+ return [];
4105
+ try {
4106
+ const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4107
+ const profiles = [];
4108
+ for (const file of files) {
4109
+ try {
4110
+ const content = readFileSync2(join2(PROFILES_DIR, file), "utf-8");
4111
+ profiles.push(JSON.parse(content));
4112
+ } catch {}
4113
+ }
4114
+ return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
4115
+ } catch {
4116
+ return [];
4117
+ }
4118
+ }
4119
+ function touchProfile(id) {
4120
+ const profile = getProfile(id);
4121
+ if (!profile)
4122
+ return;
4123
+ profile.last_seen_at = new Date().toISOString();
4124
+ writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4125
+ `);
4126
+ }
4127
+ function exportProfiles() {
4128
+ return listProfiles();
4129
+ }
4130
+ function importProfiles(profiles) {
4131
+ ensureProfilesDir();
4132
+ let imported = 0;
4133
+ let skipped = 0;
4134
+ for (const profile of profiles) {
4135
+ if (!profile.agent_id || !profile.agent_type) {
4136
+ skipped++;
4137
+ continue;
4138
+ }
4139
+ const path = profilePath(profile.agent_id);
4140
+ if (existsSync2(path)) {
4141
+ skipped++;
4142
+ continue;
4143
+ }
4144
+ writeFileSync2(path, JSON.stringify(profile, null, 2) + `
4145
+ `);
4146
+ imported++;
4147
+ }
4148
+ return { imported, skipped };
4149
+ }
4150
+ var PROFILES_DIR;
4151
+ var init_profiles = __esm(() => {
4152
+ PROFILES_DIR = join2(homedir2(), ".hooks", "profiles");
4153
+ });
4154
+
4027
4155
  // src/mcp/server.ts
4028
4156
  var exports_server = {};
4029
4157
  __export(exports_server, {
@@ -4037,8 +4165,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4037
4165
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4038
4166
  import { z } from "zod";
4039
4167
  import { createServer } from "http";
4040
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4041
- import { join as join2, dirname as dirname2 } from "path";
4168
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4169
+ import { join as join3, dirname as dirname2 } from "path";
4042
4170
  import { fileURLToPath as fileURLToPath2 } from "url";
4043
4171
  function formatInstallResults(results, extra) {
4044
4172
  const installed = results.filter((r) => r.success).map((r) => r.hook);
@@ -4085,10 +4213,11 @@ function createHooksServer() {
4085
4213
  server.tool("hooks_install", "Install one or more hooks by registering them in agent settings", {
4086
4214
  hooks: z.array(z.string()).describe("Hook names to install"),
4087
4215
  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 });
4216
+ overwrite: z.boolean().default(false).describe("Overwrite if already installed"),
4217
+ profile: z.string().optional().describe("Agent profile ID to scope hooks to")
4218
+ }, async ({ hooks, scope, overwrite, profile }) => {
4219
+ const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
4220
+ return formatInstallResults(results, { scope, profile });
4092
4221
  });
4093
4222
  server.tool("hooks_install_category", "Install all hooks in a category", {
4094
4223
  category: z.string().describe("Category name"),
@@ -4123,7 +4252,7 @@ function createHooksServer() {
4123
4252
  const settingsPath = getSettingsPath(scope);
4124
4253
  const issues = [];
4125
4254
  const healthy = [];
4126
- const settingsExist = existsSync2(settingsPath);
4255
+ const settingsExist = existsSync3(settingsPath);
4127
4256
  if (!settingsExist) {
4128
4257
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4129
4258
  }
@@ -4136,15 +4265,18 @@ function createHooksServer() {
4136
4265
  continue;
4137
4266
  }
4138
4267
  const hookDir = getHookPath(name);
4139
- if (!existsSync2(join2(hookDir, "src", "hook.ts"))) {
4268
+ if (!existsSync3(join3(hookDir, "src", "hook.ts"))) {
4140
4269
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4141
4270
  hookHealthy = false;
4142
4271
  }
4143
4272
  if (meta && settingsExist) {
4144
4273
  try {
4145
- const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
4274
+ const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4146
4275
  const eventHooks = settings.hooks?.[meta.event] || [];
4147
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
4276
+ const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
4277
+ const match = h.command?.match(/^hooks run (\w+)/);
4278
+ return match && match[1] === name;
4279
+ }));
4148
4280
  if (!found) {
4149
4281
  issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
4150
4282
  hookHealthy = false;
@@ -4170,10 +4302,10 @@ function createHooksServer() {
4170
4302
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4171
4303
  }
4172
4304
  const hookPath = getHookPath(name);
4173
- const readmePath = join2(hookPath, "README.md");
4305
+ const readmePath = join3(hookPath, "README.md");
4174
4306
  let readme = "";
4175
- if (existsSync2(readmePath)) {
4176
- readme = readFileSync2(readmePath, "utf-8");
4307
+ if (existsSync3(readmePath)) {
4308
+ readme = readFileSync3(readmePath, "utf-8");
4177
4309
  }
4178
4310
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
4179
4311
  }
@@ -4211,6 +4343,100 @@ function createHooksServer() {
4211
4343
  });
4212
4344
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
4213
4345
  });
4346
+ server.tool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
4347
+ name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
4348
+ input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
4349
+ profile: z.string().optional().describe("Agent profile ID to inject into hook input"),
4350
+ timeout_ms: z.number().default(1e4).describe("Timeout in milliseconds (default: 10000)")
4351
+ }, async ({ name, input, profile, timeout_ms }) => {
4352
+ const meta = getHook(name);
4353
+ if (!meta) {
4354
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4355
+ }
4356
+ const hookDir = getHookPath(name);
4357
+ const hookScript = join3(hookDir, "src", "hook.ts");
4358
+ if (!existsSync3(hookScript)) {
4359
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
4360
+ }
4361
+ let hookInput = { ...input };
4362
+ if (profile) {
4363
+ const p = getProfile(profile);
4364
+ if (p) {
4365
+ hookInput.agent = {
4366
+ agent_id: p.agent_id,
4367
+ agent_type: p.agent_type,
4368
+ name: p.name,
4369
+ preferences: p.preferences
4370
+ };
4371
+ }
4372
+ }
4373
+ const proc = Bun.spawn(["bun", "run", hookScript], {
4374
+ stdin: new Response(JSON.stringify(hookInput)),
4375
+ stdout: "pipe",
4376
+ stderr: "pipe",
4377
+ env: process.env
4378
+ });
4379
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeout_ms));
4380
+ const result = await Promise.race([
4381
+ Promise.all([
4382
+ new Response(proc.stdout).text(),
4383
+ new Response(proc.stderr).text(),
4384
+ proc.exited
4385
+ ]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
4386
+ timeoutPromise.then(() => {
4387
+ proc.kill();
4388
+ return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
4389
+ })
4390
+ ]);
4391
+ let output = {};
4392
+ try {
4393
+ output = JSON.parse(result.stdout);
4394
+ } catch {
4395
+ output = result.stdout ? { raw: result.stdout } : {};
4396
+ }
4397
+ return {
4398
+ content: [{
4399
+ type: "text",
4400
+ text: JSON.stringify({
4401
+ hook: name,
4402
+ output,
4403
+ stderr: result.stderr || undefined,
4404
+ exitCode: result.exitCode,
4405
+ ...result.timedOut ? { timedOut: true, timeout_ms } : {}
4406
+ })
4407
+ }]
4408
+ };
4409
+ });
4410
+ server.tool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
4411
+ hooks: z.array(z.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
4412
+ scope: z.enum(["global", "project"]).default("global").describe("Scope to update")
4413
+ }, async ({ hooks, scope }) => {
4414
+ const installed = getRegisteredHooks(scope);
4415
+ const toUpdate = hooks && hooks.length > 0 ? hooks : installed;
4416
+ if (toUpdate.length === 0) {
4417
+ return { content: [{ type: "text", text: JSON.stringify({ updated: [], error: "No hooks installed" }) }] };
4418
+ }
4419
+ const results = toUpdate.map((name) => {
4420
+ if (!installed.includes(name)) {
4421
+ return { hook: name, success: false, error: "Not installed" };
4422
+ }
4423
+ return installHook(name, { scope, overwrite: true });
4424
+ });
4425
+ const updated = results.filter((r) => r.success).map((r) => r.hook);
4426
+ const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
4427
+ return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
4428
+ });
4429
+ server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
4430
+ agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
4431
+ name: z.string().optional().describe("Optional display name for the agent")
4432
+ }, async ({ agent_type, name }) => {
4433
+ const profile = createProfile({ agent_type, name });
4434
+ return { content: [{ type: "text", text: JSON.stringify(profile) }] };
4435
+ });
4436
+ server.tool("hooks_profiles", "List all registered agent profiles", {}, async () => {
4437
+ const profiles = listProfiles();
4438
+ return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
4439
+ });
4214
4440
  return server;
4215
4441
  }
4216
4442
  async function startSSEServer(port = MCP_PORT) {
@@ -4254,8 +4480,9 @@ var __dirname3, pkg, MCP_PORT = 39427;
4254
4480
  var init_server = __esm(() => {
4255
4481
  init_registry();
4256
4482
  init_installer();
4483
+ init_profiles();
4257
4484
  __dirname3 = dirname2(fileURLToPath2(import.meta.url));
4258
- pkg = JSON.parse(readFileSync2(join2(__dirname3, "..", "..", "package.json"), "utf-8"));
4485
+ pkg = JSON.parse(readFileSync3(join3(__dirname3, "..", "..", "package.json"), "utf-8"));
4259
4486
  });
4260
4487
 
4261
4488
  // src/cli/index.tsx
@@ -4279,8 +4506,8 @@ var {
4279
4506
 
4280
4507
  // src/cli/index.tsx
4281
4508
  import chalk2 from "chalk";
4282
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4283
- import { join as join3, dirname as dirname3 } from "path";
4509
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4510
+ import { join as join4, dirname as dirname3 } from "path";
4284
4511
  import { fileURLToPath as fileURLToPath3 } from "url";
4285
4512
 
4286
4513
  // src/cli/components/App.tsx
@@ -5455,35 +5682,105 @@ function App({ initialHooks, overwrite = false }) {
5455
5682
  // src/cli/index.tsx
5456
5683
  init_registry();
5457
5684
  init_installer();
5685
+ init_profiles();
5458
5686
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5459
5687
  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"));
5688
+ var pkgPath = existsSync4(join4(__dirname4, "..", "package.json")) ? join4(__dirname4, "..", "package.json") : join4(__dirname4, "..", "..", "package.json");
5689
+ var pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
5462
5690
  var program2 = new Command;
5463
5691
  function resolveScope(options) {
5464
5692
  if (options.project)
5465
5693
  return "project";
5466
5694
  return "global";
5467
5695
  }
5696
+ function resolveTarget(options) {
5697
+ if (options.target === "gemini")
5698
+ return "gemini";
5699
+ if (options.target === "all")
5700
+ return "all";
5701
+ return "claude";
5702
+ }
5703
+ function editDistance(a, b) {
5704
+ const m = a.length, n = b.length;
5705
+ const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
5706
+ for (let j = 0;j <= n; j++)
5707
+ dp[0][j] = j;
5708
+ for (let i = 1;i <= m; i++) {
5709
+ for (let j = 1;j <= n; j++) {
5710
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
5711
+ }
5712
+ }
5713
+ return dp[m][n];
5714
+ }
5715
+ function suggestHooks(name, max = 3) {
5716
+ return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name: n }) => n);
5717
+ }
5468
5718
  program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
5469
5719
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
5470
5720
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
5471
5721
  });
5472
- program2.command("run").argument("<hook>", "Hook to run").description("Execute a hook (called by AI coding agents)").action(async (hook) => {
5722
+ 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) => {
5723
+ const agentType = options.agent;
5724
+ if (!["claude", "gemini", "custom"].includes(agentType)) {
5725
+ if (options.json) {
5726
+ console.log(JSON.stringify({ error: `Invalid agent type: ${options.agent}`, valid: ["claude", "gemini", "custom"] }));
5727
+ } else {
5728
+ console.log(chalk2.red(`Invalid agent type: ${options.agent}`));
5729
+ console.log(chalk2.dim("Valid types: claude, gemini, custom"));
5730
+ }
5731
+ return;
5732
+ }
5733
+ const profile = createProfile({ agent_type: agentType, name: options.name });
5734
+ if (options.json) {
5735
+ console.log(JSON.stringify(profile));
5736
+ return;
5737
+ }
5738
+ console.log(chalk2.green(`
5739
+ \u2713 Agent profile created
5740
+ `));
5741
+ console.log(` ${chalk2.dim("Agent ID:")} ${chalk2.bold(profile.agent_id)}`);
5742
+ console.log(` ${chalk2.dim("Type:")} ${profile.agent_type}`);
5743
+ if (profile.name) {
5744
+ console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
5745
+ }
5746
+ console.log(` ${chalk2.dim("Profile:")} ~/.hooks/profiles/${profile.agent_id}.json`);
5747
+ console.log();
5748
+ console.log(chalk2.dim(" Install hooks with this profile:"));
5749
+ console.log(` hooks install gitguard --profile ${profile.agent_id}`);
5750
+ console.log();
5751
+ });
5752
+ 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
5753
  const meta = getHook(hook);
5474
5754
  if (!meta) {
5475
5755
  console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
5476
5756
  process.exit(1);
5477
5757
  }
5478
5758
  const hookDir = getHookPath(hook);
5479
- const hookScript = join3(hookDir, "src", "hook.ts");
5480
- if (!existsSync3(hookScript)) {
5759
+ const hookScript = join4(hookDir, "src", "hook.ts");
5760
+ if (!existsSync4(hookScript)) {
5481
5761
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
5482
5762
  process.exit(1);
5483
5763
  }
5484
5764
  const stdin = await new Response(Bun.stdin.stream()).text();
5765
+ let hookStdin = stdin;
5766
+ if (options.profile) {
5767
+ const profile = getProfile(options.profile);
5768
+ if (profile) {
5769
+ touchProfile(options.profile);
5770
+ try {
5771
+ const input = JSON.parse(stdin);
5772
+ input.agent = {
5773
+ agent_id: profile.agent_id,
5774
+ agent_type: profile.agent_type,
5775
+ name: profile.name,
5776
+ preferences: profile.preferences
5777
+ };
5778
+ hookStdin = JSON.stringify(input);
5779
+ } catch {}
5780
+ }
5781
+ }
5485
5782
  const proc = Bun.spawn(["bun", "run", hookScript], {
5486
- stdin: new Response(stdin),
5783
+ stdin: new Response(hookStdin),
5487
5784
  stdout: "pipe",
5488
5785
  stderr: "pipe",
5489
5786
  env: process.env
@@ -5497,8 +5794,9 @@ program2.command("run").argument("<hook>", "Hook to run").description("Execute a
5497
5794
  process.stderr.write(stderr);
5498
5795
  process.exit(exitCode);
5499
5796
  });
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) => {
5797
+ 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("-t, --target <target>", "Agent target: claude, gemini, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
5501
5798
  const scope = resolveScope(options);
5799
+ const target = resolveTarget(options);
5502
5800
  let toInstall = hooks;
5503
5801
  if (options.all) {
5504
5802
  toInstall = HOOKS.map((h) => h.name);
@@ -5519,9 +5817,38 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
5519
5817
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
5520
5818
  return;
5521
5819
  }
5820
+ if (options.dryRun) {
5821
+ const known = toInstall.filter((n) => getHook(n));
5822
+ const unknown = toInstall.filter((n) => !getHook(n));
5823
+ if (options.json) {
5824
+ console.log(JSON.stringify({ dryRun: true, would_install: known, unknown, scope, target }));
5825
+ return;
5826
+ }
5827
+ console.log(chalk2.bold(`
5828
+ Dry run \u2014 would install (${scope}, ${target}):
5829
+ `));
5830
+ for (const name of known) {
5831
+ const meta = getHook(name);
5832
+ console.log(chalk2.cyan(` ${name}`) + chalk2.dim(` [${meta.event}${meta.matcher ? ` ${meta.matcher}` : ""}]`));
5833
+ }
5834
+ if (unknown.length > 0) {
5835
+ console.log();
5836
+ for (const name of unknown) {
5837
+ const suggestions = suggestHooks(name);
5838
+ console.log(chalk2.red(` \u2717 unknown: ${name}`) + (suggestions.length ? chalk2.dim(` \u2014 did you mean: ${suggestions.join(", ")}?`) : ""));
5839
+ }
5840
+ }
5841
+ return;
5842
+ }
5522
5843
  const results = [];
5523
5844
  for (const name of toInstall) {
5524
- const result = installHook(name, { scope, overwrite: options.overwrite });
5845
+ if (!getHook(name)) {
5846
+ const suggestions = suggestHooks(name);
5847
+ const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
5848
+ results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
5849
+ continue;
5850
+ }
5851
+ const result = installHook(name, { scope, overwrite: options.overwrite, target, profile: options.profile });
5525
5852
  results.push(result);
5526
5853
  }
5527
5854
  if (options.json) {
@@ -5530,13 +5857,14 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
5530
5857
  failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
5531
5858
  total: results.length,
5532
5859
  success: results.filter((r) => r.success).length,
5533
- scope
5860
+ scope,
5861
+ target
5534
5862
  }));
5535
5863
  return;
5536
5864
  }
5537
5865
  const settingsFile = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
5538
5866
  console.log(chalk2.bold(`
5539
- Installing hooks (${scope})...
5867
+ Installing hooks (${scope}, ${target})...
5540
5868
  `));
5541
5869
  for (const result of results) {
5542
5870
  if (result.success) {
@@ -5545,6 +5873,9 @@ Installing hooks (${scope})...
5545
5873
  if (meta) {
5546
5874
  console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hooks run ${result.hook}`));
5547
5875
  }
5876
+ if (result.conflict) {
5877
+ console.log(chalk2.yellow(` \u26A0 Warning: ${result.conflict}`));
5878
+ }
5548
5879
  } else {
5549
5880
  console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
5550
5881
  }
@@ -5552,23 +5883,24 @@ Installing hooks (${scope})...
5552
5883
  console.log(chalk2.dim(`
5553
5884
  Registered in ${settingsFile}`));
5554
5885
  });
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) => {
5886
+ 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
5887
  const scope = resolveScope(options);
5557
5888
  if (options.registered || options.installed) {
5558
- const registered = getRegisteredHooks(scope);
5889
+ const target = options.target === "gemini" ? "gemini" : "claude";
5890
+ const registered = getRegisteredHooksForTarget(scope, target);
5559
5891
  if (options.json) {
5560
5892
  console.log(JSON.stringify(registered.map((name) => {
5561
5893
  const meta = getHook(name);
5562
- return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope };
5894
+ return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope, target };
5563
5895
  })));
5564
5896
  return;
5565
5897
  }
5566
5898
  if (registered.length === 0) {
5567
- console.log(chalk2.dim(`No hooks registered (${scope})`));
5899
+ console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
5568
5900
  return;
5569
5901
  }
5570
5902
  console.log(chalk2.bold(`
5571
- Registered hooks \u2014 ${scope} (${registered.length}):
5903
+ Registered hooks \u2014 ${scope}/${target} (${registered.length}):
5572
5904
  `));
5573
5905
  for (const name of registered) {
5574
5906
  const meta = getHook(name);
@@ -5638,17 +5970,28 @@ Found ${results.length} hook(s):
5638
5970
  console.log(` ${h.description}`);
5639
5971
  }
5640
5972
  });
5641
- program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
5973
+ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
5642
5974
  const scope = resolveScope(options);
5643
- const removed = removeHook(hook, scope);
5975
+ const target = resolveTarget(options);
5976
+ if (!getHook(hook)) {
5977
+ const suggestions = suggestHooks(hook);
5978
+ const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
5979
+ if (options.json) {
5980
+ console.log(JSON.stringify({ hook, removed: false, scope, target, error: `Hook '${hook}' not found${hint}`, suggestions }));
5981
+ } else {
5982
+ console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
5983
+ }
5984
+ return;
5985
+ }
5986
+ const removed = removeHook(hook, scope, target);
5644
5987
  if (options.json) {
5645
- console.log(JSON.stringify({ hook, removed, scope }));
5988
+ console.log(JSON.stringify({ hook, removed, scope, target }));
5646
5989
  return;
5647
5990
  }
5648
5991
  if (removed) {
5649
- console.log(chalk2.green(`\u2713 Removed ${hook} (${scope})`));
5992
+ console.log(chalk2.green(`\u2713 Removed ${hook} (${scope}, ${target})`));
5650
5993
  } else {
5651
- console.log(chalk2.red(`\u2717 ${hook} is not installed (${scope})`));
5994
+ console.log(chalk2.red(`\u2717 ${hook} is not installed (${scope}, ${target})`));
5652
5995
  }
5653
5996
  });
5654
5997
  program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
@@ -5671,10 +6014,12 @@ Categories:
5671
6014
  program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
5672
6015
  const meta = getHook(hook);
5673
6016
  if (!meta) {
6017
+ const suggestions = suggestHooks(hook);
6018
+ const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
5674
6019
  if (options.json) {
5675
- console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
6020
+ console.log(JSON.stringify({ error: `Hook '${hook}' not found${hint}`, suggestions }));
5676
6021
  } else {
5677
- console.log(chalk2.red(`Hook '${hook}' not found`));
6022
+ console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
5678
6023
  }
5679
6024
  return;
5680
6025
  }
@@ -5712,7 +6057,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
5712
6057
  const settingsPath = getSettingsPath(scope);
5713
6058
  const issues = [];
5714
6059
  const healthy = [];
5715
- const settingsExist = existsSync3(settingsPath);
6060
+ const settingsExist = existsSync4(settingsPath);
5716
6061
  if (!settingsExist) {
5717
6062
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
5718
6063
  }
@@ -5726,16 +6071,19 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
5726
6071
  continue;
5727
6072
  }
5728
6073
  const hookDir = getHookPath(name);
5729
- const hookScript = join3(hookDir, "src", "hook.ts");
5730
- if (!existsSync3(hookScript)) {
6074
+ const hookScript = join4(hookDir, "src", "hook.ts");
6075
+ if (!existsSync4(hookScript)) {
5731
6076
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
5732
6077
  hookHealthy = false;
5733
6078
  }
5734
6079
  if (meta && settingsExist) {
5735
6080
  try {
5736
- const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
6081
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
5737
6082
  const eventHooks = settings.hooks?.[meta.event] || [];
5738
- const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
6083
+ const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
6084
+ const match = h.command?.match(/^hooks run (\w+)/);
6085
+ return match && match[1] === name;
6086
+ }));
5739
6087
  if (!found) {
5740
6088
  issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
5741
6089
  hookHealthy = false;
@@ -5828,10 +6176,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
5828
6176
  return;
5829
6177
  }
5830
6178
  const hookPath = getHookPath(hook);
5831
- const readmePath = join3(hookPath, "README.md");
6179
+ const readmePath = join4(hookPath, "README.md");
5832
6180
  let readme = "";
5833
- if (existsSync3(readmePath)) {
5834
- readme = readFileSync3(readmePath, "utf-8");
6181
+ if (existsSync4(readmePath)) {
6182
+ readme = readFileSync4(readmePath, "utf-8");
5835
6183
  }
5836
6184
  if (options.json) {
5837
6185
  console.log(JSON.stringify({ ...meta, readme }));
@@ -5934,6 +6282,118 @@ ${meta.displayName} v${meta.version}
5934
6282
  console.log(` hooks docs --json Machine-readable documentation`);
5935
6283
  console.log();
5936
6284
  });
6285
+ 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) => {
6286
+ const current = pkg2.version;
6287
+ let pm = "npm";
6288
+ try {
6289
+ const which = Bun.spawnSync(["which", "bun"]);
6290
+ if (which.exitCode === 0)
6291
+ pm = "bun";
6292
+ } catch {}
6293
+ if (options.check) {
6294
+ const proc2 = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
6295
+ const latest2 = new TextDecoder().decode(proc2.stdout).trim();
6296
+ if (!latest2) {
6297
+ if (options.json) {
6298
+ console.log(JSON.stringify({ error: "Failed to fetch latest version" }));
6299
+ } else {
6300
+ console.log(chalk2.red("Failed to fetch latest version from npm registry."));
6301
+ }
6302
+ process.exit(1);
6303
+ }
6304
+ const upToDate = current === latest2;
6305
+ if (options.json) {
6306
+ console.log(JSON.stringify({ current, latest: latest2, upToDate }));
6307
+ } else if (upToDate) {
6308
+ console.log(chalk2.green(`\u2713 Already on latest version (${current})`));
6309
+ } else {
6310
+ console.log(chalk2.yellow(`Update available: ${current} \u2192 ${latest2}`));
6311
+ console.log(chalk2.dim(` Run: hooks upgrade`));
6312
+ }
6313
+ return;
6314
+ }
6315
+ const installCmd = pm === "bun" ? ["bun", "install", "-g", "@hasna/hooks@latest"] : ["npm", "install", "-g", "@hasna/hooks@latest"];
6316
+ if (!options.json) {
6317
+ console.log(chalk2.bold(`
6318
+ Upgrading @hasna/hooks (${pm})...
6319
+ `));
6320
+ console.log(chalk2.dim(` $ ${installCmd.join(" ")}
6321
+ `));
6322
+ }
6323
+ const proc = Bun.spawn(installCmd, {
6324
+ stdout: options.json ? "pipe" : "inherit",
6325
+ stderr: options.json ? "pipe" : "inherit",
6326
+ env: process.env
6327
+ });
6328
+ const exitCode = await proc.exited;
6329
+ if (exitCode !== 0) {
6330
+ if (options.json) {
6331
+ console.log(JSON.stringify({ current, updated: false, error: `${pm} exited with code ${exitCode}` }));
6332
+ } else {
6333
+ console.log(chalk2.red(`
6334
+ \u2717 Upgrade failed (exit code ${exitCode})`));
6335
+ }
6336
+ process.exit(exitCode);
6337
+ }
6338
+ const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
6339
+ const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
6340
+ if (options.json) {
6341
+ console.log(JSON.stringify({ current, latest, updated: true }));
6342
+ } else {
6343
+ console.log(chalk2.green(`
6344
+ \u2713 Upgraded: ${current} \u2192 ${latest}`));
6345
+ }
6346
+ });
6347
+ program2.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
6348
+ const profiles = exportProfiles();
6349
+ const json = JSON.stringify(profiles, null, 2);
6350
+ if (options.output) {
6351
+ const { writeFileSync: writeFileSync3 } = await import("fs");
6352
+ writeFileSync3(options.output, json + `
6353
+ `);
6354
+ console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
6355
+ } else {
6356
+ console.log(json);
6357
+ }
6358
+ });
6359
+ program2.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
6360
+ let raw;
6361
+ if (file === "-") {
6362
+ raw = await new Response(Bun.stdin.stream()).text();
6363
+ } else {
6364
+ const { readFileSync: readFileSync5 } = await import("fs");
6365
+ try {
6366
+ raw = readFileSync5(file, "utf-8");
6367
+ } catch {
6368
+ if (options.json) {
6369
+ console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
6370
+ } else {
6371
+ console.log(chalk2.red(`\u2717 Cannot read file: ${file}`));
6372
+ }
6373
+ return;
6374
+ }
6375
+ }
6376
+ let profiles;
6377
+ try {
6378
+ const parsed = JSON.parse(raw);
6379
+ profiles = Array.isArray(parsed) ? parsed : [parsed];
6380
+ } catch {
6381
+ if (options.json) {
6382
+ console.log(JSON.stringify({ error: "Invalid JSON" }));
6383
+ } else {
6384
+ console.log(chalk2.red("\u2717 Invalid JSON"));
6385
+ }
6386
+ return;
6387
+ }
6388
+ const result = importProfiles(profiles);
6389
+ if (options.json) {
6390
+ console.log(JSON.stringify(result));
6391
+ } else {
6392
+ console.log(chalk2.green(`\u2713 Imported ${result.imported} profile(s)`));
6393
+ if (result.skipped > 0)
6394
+ console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
6395
+ }
6396
+ });
5937
6397
  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
6398
  if (options.stdio) {
5939
6399
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));