@hasna/hooks 0.0.4 ā 0.0.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 +351 -19
- package/dist/index.js +4 -0
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/package.json +2 -1
package/bin/index.js
CHANGED
|
@@ -3519,6 +3519,9 @@ var {
|
|
|
3519
3519
|
|
|
3520
3520
|
// src/cli/index.tsx
|
|
3521
3521
|
import chalk2 from "chalk";
|
|
3522
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3523
|
+
import { join as join2 } from "path";
|
|
3524
|
+
import { homedir as homedir2 } from "os";
|
|
3522
3525
|
|
|
3523
3526
|
// src/cli/components/App.tsx
|
|
3524
3527
|
import { useState as useState7 } from "react";
|
|
@@ -4572,6 +4575,10 @@ function installHook(name, options = {}) {
|
|
|
4572
4575
|
if (!existsSync(destDir)) {
|
|
4573
4576
|
mkdirSync(destDir, { recursive: true });
|
|
4574
4577
|
}
|
|
4578
|
+
if (overwrite && existsSync(destPath)) {
|
|
4579
|
+
const { rmSync } = __require("fs");
|
|
4580
|
+
rmSync(destPath, { recursive: true, force: true });
|
|
4581
|
+
}
|
|
4575
4582
|
cpSync(sourcePath, destPath, { recursive: true });
|
|
4576
4583
|
registerHookInSettings(shortName);
|
|
4577
4584
|
updateHooksIndex(destDir);
|
|
@@ -5028,36 +5035,72 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
5028
5035
|
// src/cli/index.tsx
|
|
5029
5036
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5030
5037
|
var program2 = new Command;
|
|
5031
|
-
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.
|
|
5038
|
+
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.5");
|
|
5032
5039
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
5033
5040
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5034
5041
|
});
|
|
5035
|
-
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).description("Install one or more hooks").action((hooks, options) => {
|
|
5036
|
-
|
|
5042
|
+
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("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
|
|
5043
|
+
let toInstall = hooks;
|
|
5044
|
+
if (options.all) {
|
|
5045
|
+
toInstall = HOOKS.map((h) => h.name);
|
|
5046
|
+
} else if (options.category) {
|
|
5047
|
+
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
5048
|
+
if (!category) {
|
|
5049
|
+
if (options.json) {
|
|
5050
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}`, available: [...CATEGORIES] }));
|
|
5051
|
+
} else {
|
|
5052
|
+
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
5053
|
+
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
5054
|
+
}
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
toInstall = getHooksByCategory(category).map((h) => h.name);
|
|
5058
|
+
}
|
|
5059
|
+
if (toInstall.length === 0) {
|
|
5037
5060
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5038
5061
|
return;
|
|
5039
5062
|
}
|
|
5063
|
+
const results = [];
|
|
5064
|
+
for (const name of toInstall) {
|
|
5065
|
+
const result = installHook(name, { overwrite: options.overwrite });
|
|
5066
|
+
results.push(result);
|
|
5067
|
+
}
|
|
5068
|
+
if (options.json) {
|
|
5069
|
+
console.log(JSON.stringify({
|
|
5070
|
+
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
5071
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
5072
|
+
total: results.length,
|
|
5073
|
+
success: results.filter((r) => r.success).length
|
|
5074
|
+
}));
|
|
5075
|
+
return;
|
|
5076
|
+
}
|
|
5040
5077
|
console.log(chalk2.bold(`
|
|
5041
5078
|
Installing hooks...
|
|
5042
5079
|
`));
|
|
5043
|
-
for (const
|
|
5044
|
-
const result = installHook(name, { overwrite: options.overwrite });
|
|
5080
|
+
for (const result of results) {
|
|
5045
5081
|
if (result.success) {
|
|
5046
|
-
const meta = getHook(
|
|
5047
|
-
console.log(chalk2.green(`\u2713 ${
|
|
5082
|
+
const meta = getHook(result.hook);
|
|
5083
|
+
console.log(chalk2.green(`\u2713 ${result.hook}`));
|
|
5048
5084
|
if (meta) {
|
|
5049
|
-
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hook-${
|
|
5085
|
+
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hook-${result.hook}`));
|
|
5050
5086
|
}
|
|
5051
5087
|
} else {
|
|
5052
|
-
console.log(chalk2.red(`\u2717 ${
|
|
5088
|
+
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
5053
5089
|
}
|
|
5054
5090
|
}
|
|
5055
5091
|
console.log(chalk2.dim(`
|
|
5056
5092
|
Hooks installed to .hooks/ and registered in ~/.claude/settings.json`));
|
|
5057
5093
|
});
|
|
5058
|
-
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 hooks registered in Claude settings", false).description("List available or installed hooks").action((options) => {
|
|
5094
|
+
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 hooks registered in Claude settings", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
|
|
5059
5095
|
if (options.registered) {
|
|
5060
5096
|
const registered = getRegisteredHooks();
|
|
5097
|
+
if (options.json) {
|
|
5098
|
+
console.log(JSON.stringify(registered.map((name) => {
|
|
5099
|
+
const meta = getHook(name);
|
|
5100
|
+
return { name, event: meta?.event, description: meta?.description };
|
|
5101
|
+
})));
|
|
5102
|
+
return;
|
|
5103
|
+
}
|
|
5061
5104
|
if (registered.length === 0) {
|
|
5062
5105
|
console.log(chalk2.dim("No hooks registered in Claude settings"));
|
|
5063
5106
|
return;
|
|
@@ -5073,6 +5116,13 @@ Registered hooks (${registered.length}):
|
|
|
5073
5116
|
}
|
|
5074
5117
|
if (options.installed) {
|
|
5075
5118
|
const installed = getInstalledHooks();
|
|
5119
|
+
if (options.json) {
|
|
5120
|
+
console.log(JSON.stringify(installed.map((name) => {
|
|
5121
|
+
const meta = getHook(name);
|
|
5122
|
+
return { name, event: meta?.event, version: meta?.version, description: meta?.description };
|
|
5123
|
+
})));
|
|
5124
|
+
return;
|
|
5125
|
+
}
|
|
5076
5126
|
if (installed.length === 0) {
|
|
5077
5127
|
console.log(chalk2.dim("No hooks installed"));
|
|
5078
5128
|
return;
|
|
@@ -5088,11 +5138,19 @@ Installed hooks (${installed.length}):
|
|
|
5088
5138
|
if (options.category) {
|
|
5089
5139
|
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
5090
5140
|
if (!category) {
|
|
5091
|
-
|
|
5092
|
-
|
|
5141
|
+
if (options.json) {
|
|
5142
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}`, available: [...CATEGORIES] }));
|
|
5143
|
+
} else {
|
|
5144
|
+
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
5145
|
+
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
5146
|
+
}
|
|
5093
5147
|
return;
|
|
5094
5148
|
}
|
|
5095
5149
|
const hooks = getHooksByCategory(category);
|
|
5150
|
+
if (options.json) {
|
|
5151
|
+
console.log(JSON.stringify(hooks));
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5096
5154
|
console.log(chalk2.bold(`
|
|
5097
5155
|
${category} (${hooks.length}):
|
|
5098
5156
|
`));
|
|
@@ -5101,6 +5159,14 @@ ${category} (${hooks.length}):
|
|
|
5101
5159
|
}
|
|
5102
5160
|
return;
|
|
5103
5161
|
}
|
|
5162
|
+
if (options.json) {
|
|
5163
|
+
const result = {};
|
|
5164
|
+
for (const category of CATEGORIES) {
|
|
5165
|
+
result[category] = getHooksByCategory(category);
|
|
5166
|
+
}
|
|
5167
|
+
console.log(JSON.stringify(result));
|
|
5168
|
+
return;
|
|
5169
|
+
}
|
|
5104
5170
|
console.log(chalk2.bold(`
|
|
5105
5171
|
Available hooks (${HOOKS.length}):
|
|
5106
5172
|
`));
|
|
@@ -5113,8 +5179,12 @@ Available hooks (${HOOKS.length}):
|
|
|
5113
5179
|
console.log();
|
|
5114
5180
|
}
|
|
5115
5181
|
});
|
|
5116
|
-
program2.command("search").argument("<query>", "Search term").description("Search for hooks").action((query) => {
|
|
5182
|
+
program2.command("search").argument("<query>", "Search term").option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
|
|
5117
5183
|
const results = searchHooks(query);
|
|
5184
|
+
if (options.json) {
|
|
5185
|
+
console.log(JSON.stringify(results));
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5118
5188
|
if (results.length === 0) {
|
|
5119
5189
|
console.log(chalk2.dim(`No hooks found for "${query}"`));
|
|
5120
5190
|
return;
|
|
@@ -5127,15 +5197,27 @@ Found ${results.length} hook(s):
|
|
|
5127
5197
|
console.log(` ${h.description}`);
|
|
5128
5198
|
}
|
|
5129
5199
|
});
|
|
5130
|
-
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").description("Remove an installed hook").action((hook) => {
|
|
5200
|
+
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
|
|
5131
5201
|
const removed = removeHook(hook);
|
|
5202
|
+
if (options.json) {
|
|
5203
|
+
console.log(JSON.stringify({ hook, removed }));
|
|
5204
|
+
return;
|
|
5205
|
+
}
|
|
5132
5206
|
if (removed) {
|
|
5133
5207
|
console.log(chalk2.green(`\u2713 Removed ${hook} (unregistered from Claude settings)`));
|
|
5134
5208
|
} else {
|
|
5135
5209
|
console.log(chalk2.red(`\u2717 ${hook} is not installed`));
|
|
5136
5210
|
}
|
|
5137
5211
|
});
|
|
5138
|
-
program2.command("categories").description("List all categories").action(() => {
|
|
5212
|
+
program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
5213
|
+
if (options.json) {
|
|
5214
|
+
const result = CATEGORIES.map((cat) => ({
|
|
5215
|
+
name: cat,
|
|
5216
|
+
count: getHooksByCategory(cat).length
|
|
5217
|
+
}));
|
|
5218
|
+
console.log(JSON.stringify(result));
|
|
5219
|
+
return;
|
|
5220
|
+
}
|
|
5139
5221
|
console.log(chalk2.bold(`
|
|
5140
5222
|
Categories:
|
|
5141
5223
|
`));
|
|
@@ -5144,10 +5226,22 @@ Categories:
|
|
|
5144
5226
|
console.log(` ${category} (${count})`);
|
|
5145
5227
|
}
|
|
5146
5228
|
});
|
|
5147
|
-
program2.command("info").argument("<hook>", "Hook name").description("Show detailed info about a hook").action((hook) => {
|
|
5229
|
+
program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
|
|
5148
5230
|
const meta = getHook(hook);
|
|
5149
5231
|
if (!meta) {
|
|
5150
|
-
|
|
5232
|
+
if (options.json) {
|
|
5233
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5234
|
+
} else {
|
|
5235
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
5236
|
+
}
|
|
5237
|
+
return;
|
|
5238
|
+
}
|
|
5239
|
+
const registered = getRegisteredHooks();
|
|
5240
|
+
const isRegistered = registered.includes(meta.name);
|
|
5241
|
+
const installed = getInstalledHooks();
|
|
5242
|
+
const isInstalled = installed.includes(meta.name);
|
|
5243
|
+
if (options.json) {
|
|
5244
|
+
console.log(JSON.stringify({ ...meta, registered: isRegistered, installed: isInstalled }));
|
|
5151
5245
|
return;
|
|
5152
5246
|
}
|
|
5153
5247
|
console.log(chalk2.bold(`
|
|
@@ -5156,16 +5250,254 @@ ${meta.displayName}
|
|
|
5156
5250
|
console.log(` ${meta.description}`);
|
|
5157
5251
|
console.log();
|
|
5158
5252
|
console.log(` ${chalk2.dim("Category:")} ${meta.category}`);
|
|
5253
|
+
console.log(` ${chalk2.dim("Version:")} ${meta.version}`);
|
|
5159
5254
|
console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
|
|
5160
5255
|
console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
5161
5256
|
console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
5162
5257
|
console.log(` ${chalk2.dim("Package:")} hook-${meta.name}`);
|
|
5163
5258
|
console.log();
|
|
5164
|
-
|
|
5165
|
-
if (registered.includes(meta.name)) {
|
|
5259
|
+
if (isRegistered) {
|
|
5166
5260
|
console.log(chalk2.green(" \u25CF Registered in Claude settings"));
|
|
5167
5261
|
} else {
|
|
5168
5262
|
console.log(chalk2.dim(" \u25CB Not registered"));
|
|
5169
5263
|
}
|
|
5264
|
+
if (isInstalled) {
|
|
5265
|
+
console.log(chalk2.green(" \u25CF Installed in .hooks/"));
|
|
5266
|
+
} else {
|
|
5267
|
+
console.log(chalk2.dim(" \u25CB Not installed"));
|
|
5268
|
+
}
|
|
5269
|
+
});
|
|
5270
|
+
program2.command("doctor").option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
|
|
5271
|
+
const settingsPath = join2(homedir2(), ".claude", "settings.json");
|
|
5272
|
+
const issues = [];
|
|
5273
|
+
const healthy = [];
|
|
5274
|
+
const settingsExist = existsSync2(settingsPath);
|
|
5275
|
+
if (!settingsExist) {
|
|
5276
|
+
issues.push({ hook: "(settings)", issue: "~/.claude/settings.json not found", severity: "warning" });
|
|
5277
|
+
}
|
|
5278
|
+
const installed = getInstalledHooks();
|
|
5279
|
+
const registered = getRegisteredHooks();
|
|
5280
|
+
for (const name of installed) {
|
|
5281
|
+
const meta = getHook(name);
|
|
5282
|
+
const hookDir = join2(process.cwd(), ".hooks", `hook-${name}`);
|
|
5283
|
+
let hookHealthy = true;
|
|
5284
|
+
const srcDir = join2(hookDir, "src");
|
|
5285
|
+
if (!existsSync2(srcDir)) {
|
|
5286
|
+
issues.push({ hook: name, issue: "Missing src/ directory", severity: "error" });
|
|
5287
|
+
hookHealthy = false;
|
|
5288
|
+
}
|
|
5289
|
+
if (!existsSync2(join2(hookDir, "package.json"))) {
|
|
5290
|
+
issues.push({ hook: name, issue: "Missing package.json", severity: "error" });
|
|
5291
|
+
hookHealthy = false;
|
|
5292
|
+
}
|
|
5293
|
+
if (!registered.includes(name)) {
|
|
5294
|
+
issues.push({ hook: name, issue: "Installed but not registered in Claude settings", severity: "warning" });
|
|
5295
|
+
hookHealthy = false;
|
|
5296
|
+
}
|
|
5297
|
+
if (meta && settingsExist) {
|
|
5298
|
+
try {
|
|
5299
|
+
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
5300
|
+
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5301
|
+
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command?.includes(`hook-${name}`)));
|
|
5302
|
+
if (registered.includes(name) && !found) {
|
|
5303
|
+
issues.push({ hook: name, issue: `Registered but not under ${meta.event} event`, severity: "error" });
|
|
5304
|
+
hookHealthy = false;
|
|
5305
|
+
}
|
|
5306
|
+
} catch {}
|
|
5307
|
+
}
|
|
5308
|
+
if (hookHealthy) {
|
|
5309
|
+
healthy.push(name);
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
for (const name of registered) {
|
|
5313
|
+
if (!installed.includes(name)) {
|
|
5314
|
+
issues.push({ hook: name, issue: "Registered in settings but not installed in .hooks/", severity: "warning" });
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
if (options.json) {
|
|
5318
|
+
console.log(JSON.stringify({ healthy, issues, installed, registered }));
|
|
5319
|
+
return;
|
|
5320
|
+
}
|
|
5321
|
+
console.log(chalk2.bold(`
|
|
5322
|
+
Hook Health Check
|
|
5323
|
+
`));
|
|
5324
|
+
if (installed.length === 0 && registered.length === 0) {
|
|
5325
|
+
console.log(chalk2.dim(" No hooks installed or registered."));
|
|
5326
|
+
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
if (healthy.length > 0) {
|
|
5330
|
+
console.log(chalk2.green(` \u2713 ${healthy.length} hook(s) healthy:`));
|
|
5331
|
+
for (const name of healthy) {
|
|
5332
|
+
console.log(chalk2.green(` ${name}`));
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
if (issues.length > 0) {
|
|
5336
|
+
console.log();
|
|
5337
|
+
for (const issue of issues) {
|
|
5338
|
+
const icon = issue.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
5339
|
+
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
if (issues.length === 0) {
|
|
5343
|
+
console.log(chalk2.green(`
|
|
5344
|
+
All hooks healthy!`));
|
|
5345
|
+
}
|
|
5346
|
+
console.log();
|
|
5347
|
+
});
|
|
5348
|
+
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-j, --json", "Output as JSON", false).description("Update installed hooks to latest version from package").action((hooks, options) => {
|
|
5349
|
+
const installed = getInstalledHooks();
|
|
5350
|
+
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
5351
|
+
if (toUpdate.length === 0) {
|
|
5352
|
+
if (options.json) {
|
|
5353
|
+
console.log(JSON.stringify({ updated: [], error: "No hooks installed" }));
|
|
5354
|
+
} else {
|
|
5355
|
+
console.log(chalk2.dim("No hooks installed to update."));
|
|
5356
|
+
}
|
|
5357
|
+
return;
|
|
5358
|
+
}
|
|
5359
|
+
const results = [];
|
|
5360
|
+
for (const name of toUpdate) {
|
|
5361
|
+
if (!installed.includes(name)) {
|
|
5362
|
+
results.push({ hook: name, success: false, error: "Not installed" });
|
|
5363
|
+
continue;
|
|
5364
|
+
}
|
|
5365
|
+
const result = installHook(name, { overwrite: true });
|
|
5366
|
+
results.push(result);
|
|
5367
|
+
}
|
|
5368
|
+
if (options.json) {
|
|
5369
|
+
console.log(JSON.stringify({
|
|
5370
|
+
updated: results.filter((r) => r.success).map((r) => r.hook),
|
|
5371
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }))
|
|
5372
|
+
}));
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
console.log(chalk2.bold(`
|
|
5376
|
+
Updating hooks...
|
|
5377
|
+
`));
|
|
5378
|
+
for (const result of results) {
|
|
5379
|
+
if (result.success) {
|
|
5380
|
+
console.log(chalk2.green(`\u2713 ${result.hook} updated`));
|
|
5381
|
+
} else {
|
|
5382
|
+
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
});
|
|
5386
|
+
program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
|
|
5387
|
+
if (hook) {
|
|
5388
|
+
const meta = getHook(hook);
|
|
5389
|
+
if (!meta) {
|
|
5390
|
+
if (options.json) {
|
|
5391
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5392
|
+
} else {
|
|
5393
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
5394
|
+
}
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
const hookPath = getHookPath(hook);
|
|
5398
|
+
const readmePath = join2(hookPath, "README.md");
|
|
5399
|
+
let readme = "";
|
|
5400
|
+
if (existsSync2(readmePath)) {
|
|
5401
|
+
readme = readFileSync2(readmePath, "utf-8");
|
|
5402
|
+
}
|
|
5403
|
+
if (options.json) {
|
|
5404
|
+
console.log(JSON.stringify({ ...meta, readme }));
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
5407
|
+
console.log(chalk2.bold(`
|
|
5408
|
+
${meta.displayName} v${meta.version}
|
|
5409
|
+
`));
|
|
5410
|
+
console.log(` ${meta.description}
|
|
5411
|
+
`);
|
|
5412
|
+
console.log(chalk2.bold(" Configuration:"));
|
|
5413
|
+
console.log(` Event: ${meta.event}`);
|
|
5414
|
+
console.log(` Matcher: ${meta.matcher || "(all tools)"}`);
|
|
5415
|
+
console.log(` Package: hook-${meta.name}`);
|
|
5416
|
+
console.log();
|
|
5417
|
+
console.log(chalk2.bold(" Install:"));
|
|
5418
|
+
console.log(` hooks install ${meta.name}`);
|
|
5419
|
+
console.log();
|
|
5420
|
+
if (readme) {
|
|
5421
|
+
console.log(chalk2.bold(` README:
|
|
5422
|
+
`));
|
|
5423
|
+
const lines = readme.split(`
|
|
5424
|
+
`);
|
|
5425
|
+
for (const line of lines) {
|
|
5426
|
+
console.log(` ${line}`);
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
const generalDocs = {
|
|
5432
|
+
overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session.",
|
|
5433
|
+
events: {
|
|
5434
|
+
PreToolUse: 'Fires before a tool executes. Can block the operation by returning { "decision": "block" }.',
|
|
5435
|
+
PostToolUse: "Fires after a tool executes. Runs asynchronously, cannot block.",
|
|
5436
|
+
Stop: "Fires when a Claude Code session ends. Useful for notifications and cleanup.",
|
|
5437
|
+
Notification: "Fires on notification events like context compaction."
|
|
5438
|
+
},
|
|
5439
|
+
installation: {
|
|
5440
|
+
single: "hooks install gitguard",
|
|
5441
|
+
multiple: "hooks install gitguard checkpoint packageage",
|
|
5442
|
+
category: 'hooks install --category "Git Safety"',
|
|
5443
|
+
all: "hooks install --all"
|
|
5444
|
+
},
|
|
5445
|
+
management: {
|
|
5446
|
+
list: "hooks list",
|
|
5447
|
+
listInstalled: "hooks list --installed",
|
|
5448
|
+
listRegistered: "hooks list --registered",
|
|
5449
|
+
search: "hooks search <query>",
|
|
5450
|
+
info: "hooks info <name>",
|
|
5451
|
+
remove: "hooks remove <name>",
|
|
5452
|
+
update: "hooks update",
|
|
5453
|
+
doctor: "hooks doctor"
|
|
5454
|
+
},
|
|
5455
|
+
structure: {
|
|
5456
|
+
hooksDir: ".hooks/hook-<name>/",
|
|
5457
|
+
settings: "~/.claude/settings.json",
|
|
5458
|
+
hookSource: ".hooks/hook-<name>/src/hook.ts"
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
5461
|
+
if (options.json) {
|
|
5462
|
+
console.log(JSON.stringify(generalDocs));
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
console.log(chalk2.bold(`
|
|
5466
|
+
@hasna/hooks Documentation
|
|
5467
|
+
`));
|
|
5468
|
+
console.log(chalk2.bold(` Overview
|
|
5469
|
+
`));
|
|
5470
|
+
console.log(` ${generalDocs.overview}
|
|
5471
|
+
`);
|
|
5472
|
+
console.log(chalk2.bold(` Hook Events
|
|
5473
|
+
`));
|
|
5474
|
+
for (const [event, desc] of Object.entries(generalDocs.events)) {
|
|
5475
|
+
console.log(` ${chalk2.cyan(event)}`);
|
|
5476
|
+
console.log(` ${desc}
|
|
5477
|
+
`);
|
|
5478
|
+
}
|
|
5479
|
+
console.log(chalk2.bold(` Installation
|
|
5480
|
+
`));
|
|
5481
|
+
for (const [label, cmd] of Object.entries(generalDocs.installation)) {
|
|
5482
|
+
console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
|
|
5483
|
+
}
|
|
5484
|
+
console.log(chalk2.bold(`
|
|
5485
|
+
Management
|
|
5486
|
+
`));
|
|
5487
|
+
for (const [label, cmd] of Object.entries(generalDocs.management)) {
|
|
5488
|
+
console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
|
|
5489
|
+
}
|
|
5490
|
+
console.log(chalk2.bold(`
|
|
5491
|
+
File Structure
|
|
5492
|
+
`));
|
|
5493
|
+
for (const [label, path] of Object.entries(generalDocs.structure)) {
|
|
5494
|
+
console.log(` ${chalk2.dim(label + ":")} ${path}`);
|
|
5495
|
+
}
|
|
5496
|
+
console.log(chalk2.bold(`
|
|
5497
|
+
Hook-Specific Docs
|
|
5498
|
+
`));
|
|
5499
|
+
console.log(` hooks docs <name> View README for a specific hook`);
|
|
5500
|
+
console.log(` hooks docs --json Machine-readable documentation`);
|
|
5501
|
+
console.log();
|
|
5170
5502
|
});
|
|
5171
5503
|
program2.parse();
|
package/dist/index.js
CHANGED
|
@@ -228,6 +228,10 @@ function installHook(name, options = {}) {
|
|
|
228
228
|
if (!existsSync(destDir)) {
|
|
229
229
|
mkdirSync(destDir, { recursive: true });
|
|
230
230
|
}
|
|
231
|
+
if (overwrite && existsSync(destPath)) {
|
|
232
|
+
const { rmSync } = __require("fs");
|
|
233
|
+
rmSync(destPath, { recursive: true, force: true });
|
|
234
|
+
}
|
|
231
235
|
cpSync(sourcePath, destPath, { recursive: true });
|
|
232
236
|
registerHookInSettings(shortName);
|
|
233
237
|
updateHooksIndex(destDir);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook for service-message
|
|
4
|
+
*
|
|
5
|
+
* Runs when Claude finishes a response. Checks for unread messages
|
|
6
|
+
* and notifies Claude if there are any pending messages.
|
|
7
|
+
*
|
|
8
|
+
* This is efficient because it only runs after Claude completes a turn,
|
|
9
|
+
* not continuously polling.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { readdir } from 'fs/promises';
|
|
15
|
+
|
|
16
|
+
interface HookInput {
|
|
17
|
+
session_id: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
hook_event_name: string;
|
|
20
|
+
stop_hook_active?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Message {
|
|
24
|
+
id: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
from: string;
|
|
27
|
+
to: string;
|
|
28
|
+
project: string;
|
|
29
|
+
subject: string;
|
|
30
|
+
body: string;
|
|
31
|
+
read?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SERVICE_DIR = join(homedir(), '.service', 'service-message');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize ID to prevent path traversal attacks
|
|
38
|
+
*/
|
|
39
|
+
function sanitizeId(id: string): string | null {
|
|
40
|
+
if (!id || typeof id !== 'string') return null;
|
|
41
|
+
// Only allow alphanumeric, dash, underscore
|
|
42
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) return null;
|
|
43
|
+
// Reject path traversal attempts
|
|
44
|
+
if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readJson<T>(path: string): Promise<T | null> {
|
|
49
|
+
try {
|
|
50
|
+
const file = Bun.file(path);
|
|
51
|
+
if (await file.exists()) {
|
|
52
|
+
return await file.json();
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getUnreadMessages(agentId: string, projectId?: string): Promise<Message[]> {
|
|
59
|
+
const messages: Message[] = [];
|
|
60
|
+
const messagesDir = join(SERVICE_DIR, 'messages');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const rawProjects = projectId ? [projectId] : await readdir(messagesDir);
|
|
64
|
+
// Sanitize all project names to prevent path traversal
|
|
65
|
+
const projects = rawProjects.map(p => sanitizeId(p)).filter((p): p is string => p !== null);
|
|
66
|
+
|
|
67
|
+
for (const proj of projects) {
|
|
68
|
+
// Check inbox
|
|
69
|
+
const inboxDir = join(messagesDir, proj, 'inbox', agentId);
|
|
70
|
+
try {
|
|
71
|
+
const files = await readdir(inboxDir);
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
if (!file.endsWith('.json')) continue;
|
|
74
|
+
// Sanitize filename to prevent path traversal
|
|
75
|
+
const safeFile = sanitizeId(file.replace('.json', ''));
|
|
76
|
+
if (!safeFile) continue;
|
|
77
|
+
const msg = await readJson<Message>(join(inboxDir, `${safeFile}.json`));
|
|
78
|
+
if (msg && !msg.read) {
|
|
79
|
+
messages.push(msg);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
// Check broadcast
|
|
85
|
+
const broadcastDir = join(messagesDir, proj, 'broadcast');
|
|
86
|
+
try {
|
|
87
|
+
const files = await readdir(broadcastDir);
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (!file.endsWith('.json')) continue;
|
|
90
|
+
// Sanitize filename to prevent path traversal
|
|
91
|
+
const safeFile = sanitizeId(file.replace('.json', ''));
|
|
92
|
+
if (!safeFile) continue;
|
|
93
|
+
const msg = await readJson<Message>(join(broadcastDir, `${safeFile}.json`));
|
|
94
|
+
if (msg && !msg.read && msg.from !== agentId) {
|
|
95
|
+
messages.push(msg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
return messages.sort((a, b) => b.timestamp - a.timestamp);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
// Get agent ID from environment (set by session-start hook)
|
|
107
|
+
const rawAgentId = process.env.SMSG_AGENT_ID;
|
|
108
|
+
const rawProjectId = process.env.SMSG_PROJECT_ID;
|
|
109
|
+
|
|
110
|
+
// Sanitize IDs to prevent path traversal
|
|
111
|
+
const agentId = rawAgentId ? sanitizeId(rawAgentId) : null;
|
|
112
|
+
const projectId = rawProjectId ? sanitizeId(rawProjectId) : undefined;
|
|
113
|
+
|
|
114
|
+
if (!agentId) {
|
|
115
|
+
// Agent not configured or invalid, skip silently
|
|
116
|
+
console.log(JSON.stringify({ continue: true }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for unread messages
|
|
121
|
+
const unreadMessages = await getUnreadMessages(agentId, projectId);
|
|
122
|
+
|
|
123
|
+
if (unreadMessages.length === 0) {
|
|
124
|
+
// No messages, continue normally
|
|
125
|
+
console.log(JSON.stringify({ continue: true }));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Format message summary in a friendly way
|
|
130
|
+
const msgList = unreadMessages.slice(0, 3).map(msg => {
|
|
131
|
+
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
132
|
+
const preview = msg.body.slice(0, 60).replace(/\n/g, ' ');
|
|
133
|
+
return `šØ **${msg.subject}** from \`${msg.from}\` (${time})\n ${preview}${msg.body.length > 60 ? '...' : ''}`;
|
|
134
|
+
}).join('\n\n');
|
|
135
|
+
|
|
136
|
+
const moreNote = unreadMessages.length > 3
|
|
137
|
+
? `\n\n_...and ${unreadMessages.length - 3} more message(s)_`
|
|
138
|
+
: '';
|
|
139
|
+
|
|
140
|
+
// Inject message in a friendly, readable format
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
continue: true,
|
|
143
|
+
stopReason: `š¬ **You have ${unreadMessages.length} unread message(s):**\n\n${msgList}${moreNote}\n\nš” Use \`service-message inbox\` to see all messages or \`service-message read <id>\` to read one.`
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main().catch(() => {
|
|
148
|
+
// Don't block on errors
|
|
149
|
+
console.log(JSON.stringify({ continue: true }));
|
|
150
|
+
process.exit(0);
|
|
151
|
+
});
|