@hasna/hooks 0.2.3 → 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.
- package/README.md +302 -107
- package/bin/index.js +316 -51
- package/dist/index.js +216 -14
- package/hooks/hook-stylescheck/LICENSE +191 -0
- package/hooks/hook-stylescheck/README.md +41 -0
- package/hooks/hook-stylescheck/package.json +52 -0
- package/hooks/hook-stylescheck/src/hook.ts +213 -0
- package/hooks/hook-stylescheck/tsconfig.json +25 -0
- 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
|
|
3852
|
-
return entries.filter((entry) => !entry.hooks?.some((h) =>
|
|
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
|
-
|
|
3936
|
-
|
|
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
|
-
|
|
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
|
|
4041
|
-
import { join as
|
|
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
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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) =>
|
|
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 =
|
|
4262
|
+
const readmePath = join3(hookPath, "README.md");
|
|
4174
4263
|
let readme = "";
|
|
4175
|
-
if (
|
|
4176
|
-
readme =
|
|
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(
|
|
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
|
|
4283
|
-
import { join as
|
|
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 =
|
|
5461
|
-
var pkg2 = JSON.parse(
|
|
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("
|
|
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 =
|
|
5480
|
-
if (!
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
5730
|
-
if (!
|
|
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(
|
|
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) =>
|
|
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 =
|
|
6034
|
+
const readmePath = join4(hookPath, "README.md");
|
|
5832
6035
|
let readme = "";
|
|
5833
|
-
if (
|
|
5834
|
-
readme =
|
|
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));
|