@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/README.md +302 -107
- package/bin/index.js +521 -61
- package/dist/index.js +264 -15
- 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,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
|
|
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
|
-
|
|
3936
|
-
|
|
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
|
-
|
|
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
|
|
4041
|
-
import { join as
|
|
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
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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) =>
|
|
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 =
|
|
4305
|
+
const readmePath = join3(hookPath, "README.md");
|
|
4174
4306
|
let readme = "";
|
|
4175
|
-
if (
|
|
4176
|
-
readme =
|
|
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(
|
|
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
|
|
4283
|
-
import { join as
|
|
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 =
|
|
5461
|
-
var pkg2 = JSON.parse(
|
|
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("
|
|
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 =
|
|
5480
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
5730
|
-
if (!
|
|
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(
|
|
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) =>
|
|
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 =
|
|
6179
|
+
const readmePath = join4(hookPath, "README.md");
|
|
5832
6180
|
let readme = "";
|
|
5833
|
-
if (
|
|
5834
|
-
readme =
|
|
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));
|