@hasna/hooks 0.0.5 → 0.0.7
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/.claude/settings.json +24 -0
- package/bin/index.js +710 -462
- package/dist/index.js +46 -94
- package/package.json +3 -2
- package/.hooks/index.ts +0 -6
package/bin/index.js
CHANGED
|
@@ -17,6 +17,16 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
20
30
|
var __require = import.meta.require;
|
|
21
31
|
|
|
22
32
|
// node_modules/commander/lib/error.js
|
|
@@ -1858,6 +1868,180 @@ var require_commander = __commonJS((exports) => {
|
|
|
1858
1868
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
1859
1869
|
});
|
|
1860
1870
|
|
|
1871
|
+
// src/lib/registry.ts
|
|
1872
|
+
function getHooksByCategory(category) {
|
|
1873
|
+
return HOOKS.filter((h) => h.category === category);
|
|
1874
|
+
}
|
|
1875
|
+
function searchHooks(query) {
|
|
1876
|
+
const q = query.toLowerCase();
|
|
1877
|
+
return HOOKS.filter((h) => h.name.toLowerCase().includes(q) || h.displayName.toLowerCase().includes(q) || h.description.toLowerCase().includes(q) || h.tags.some((t) => t.includes(q)));
|
|
1878
|
+
}
|
|
1879
|
+
function getHook(name) {
|
|
1880
|
+
return HOOKS.find((h) => h.name === name);
|
|
1881
|
+
}
|
|
1882
|
+
var CATEGORIES, HOOKS;
|
|
1883
|
+
var init_registry = __esm(() => {
|
|
1884
|
+
CATEGORIES = [
|
|
1885
|
+
"Git Safety",
|
|
1886
|
+
"Code Quality",
|
|
1887
|
+
"Security",
|
|
1888
|
+
"Notifications",
|
|
1889
|
+
"Context Management"
|
|
1890
|
+
];
|
|
1891
|
+
HOOKS = [
|
|
1892
|
+
{
|
|
1893
|
+
name: "gitguard",
|
|
1894
|
+
displayName: "Git Guard",
|
|
1895
|
+
description: "Blocks destructive git operations like reset --hard, push --force, clean -f",
|
|
1896
|
+
version: "0.1.0",
|
|
1897
|
+
category: "Git Safety",
|
|
1898
|
+
event: "PreToolUse",
|
|
1899
|
+
matcher: "Bash",
|
|
1900
|
+
tags: ["git", "safety", "destructive", "guard"]
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
name: "branchprotect",
|
|
1904
|
+
displayName: "Branch Protect",
|
|
1905
|
+
description: "Prevents editing files directly on main/master branch",
|
|
1906
|
+
version: "0.1.0",
|
|
1907
|
+
category: "Git Safety",
|
|
1908
|
+
event: "PreToolUse",
|
|
1909
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
1910
|
+
tags: ["git", "branch", "protection", "main"]
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
name: "checkpoint",
|
|
1914
|
+
displayName: "Checkpoint",
|
|
1915
|
+
description: "Creates shadow git snapshots before file modifications for easy rollback",
|
|
1916
|
+
version: "0.1.0",
|
|
1917
|
+
category: "Git Safety",
|
|
1918
|
+
event: "PreToolUse",
|
|
1919
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
1920
|
+
tags: ["git", "snapshot", "rollback", "backup"]
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
name: "checktests",
|
|
1924
|
+
displayName: "Check Tests",
|
|
1925
|
+
description: "Checks for missing tests after file edits",
|
|
1926
|
+
version: "0.1.6",
|
|
1927
|
+
category: "Code Quality",
|
|
1928
|
+
event: "PostToolUse",
|
|
1929
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1930
|
+
tags: ["tests", "coverage", "quality"]
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
name: "checklint",
|
|
1934
|
+
displayName: "Check Lint",
|
|
1935
|
+
description: "Runs linting after file edits and creates tasks for errors",
|
|
1936
|
+
version: "0.1.7",
|
|
1937
|
+
category: "Code Quality",
|
|
1938
|
+
event: "PostToolUse",
|
|
1939
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1940
|
+
tags: ["lint", "style", "quality"]
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
name: "checkfiles",
|
|
1944
|
+
displayName: "Check Files",
|
|
1945
|
+
description: "Runs headless agent to review files and create tasks",
|
|
1946
|
+
version: "0.1.4",
|
|
1947
|
+
category: "Code Quality",
|
|
1948
|
+
event: "PostToolUse",
|
|
1949
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1950
|
+
tags: ["review", "files", "quality"]
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
name: "checkbugs",
|
|
1954
|
+
displayName: "Check Bugs",
|
|
1955
|
+
description: "Checks for bugs via Codex headless agent",
|
|
1956
|
+
version: "0.1.6",
|
|
1957
|
+
category: "Code Quality",
|
|
1958
|
+
event: "PostToolUse",
|
|
1959
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1960
|
+
tags: ["bugs", "analysis", "quality"]
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
name: "checkdocs",
|
|
1964
|
+
displayName: "Check Docs",
|
|
1965
|
+
description: "Checks for missing documentation and creates tasks",
|
|
1966
|
+
version: "0.2.1",
|
|
1967
|
+
category: "Code Quality",
|
|
1968
|
+
event: "PostToolUse",
|
|
1969
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1970
|
+
tags: ["docs", "documentation", "quality"]
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
name: "checktasks",
|
|
1974
|
+
displayName: "Check Tasks",
|
|
1975
|
+
description: "Validates task completion and tracks progress",
|
|
1976
|
+
version: "1.0.8",
|
|
1977
|
+
category: "Code Quality",
|
|
1978
|
+
event: "PostToolUse",
|
|
1979
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1980
|
+
tags: ["tasks", "tracking", "quality"]
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
name: "checksecurity",
|
|
1984
|
+
displayName: "Check Security",
|
|
1985
|
+
description: "Runs security checks via Claude and Codex headless agents",
|
|
1986
|
+
version: "0.1.6",
|
|
1987
|
+
category: "Security",
|
|
1988
|
+
event: "PostToolUse",
|
|
1989
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
1990
|
+
tags: ["security", "audit", "vulnerabilities"]
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
name: "packageage",
|
|
1994
|
+
displayName: "Package Age",
|
|
1995
|
+
description: "Checks package age before install to prevent typosquatting",
|
|
1996
|
+
version: "0.1.1",
|
|
1997
|
+
category: "Security",
|
|
1998
|
+
event: "PreToolUse",
|
|
1999
|
+
matcher: "Bash",
|
|
2000
|
+
tags: ["npm", "packages", "typosquatting", "supply-chain"]
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
name: "phonenotify",
|
|
2004
|
+
displayName: "Phone Notify",
|
|
2005
|
+
description: "Sends push notifications to phone via ntfy.sh",
|
|
2006
|
+
version: "0.1.0",
|
|
2007
|
+
category: "Notifications",
|
|
2008
|
+
event: "Stop",
|
|
2009
|
+
matcher: "",
|
|
2010
|
+
tags: ["notification", "phone", "push", "ntfy"]
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
name: "agentmessages",
|
|
2014
|
+
displayName: "Agent Messages",
|
|
2015
|
+
description: "Inter-agent messaging integration for service-message",
|
|
2016
|
+
version: "0.1.0",
|
|
2017
|
+
category: "Notifications",
|
|
2018
|
+
event: "Stop",
|
|
2019
|
+
matcher: "",
|
|
2020
|
+
tags: ["messaging", "agents", "inter-agent"]
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
name: "contextrefresh",
|
|
2024
|
+
displayName: "Context Refresh",
|
|
2025
|
+
description: "Re-injects important context every N prompts to prevent drift",
|
|
2026
|
+
version: "0.1.0",
|
|
2027
|
+
category: "Context Management",
|
|
2028
|
+
event: "Notification",
|
|
2029
|
+
matcher: "",
|
|
2030
|
+
tags: ["context", "memory", "prompts", "refresh"]
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
name: "precompact",
|
|
2034
|
+
displayName: "Pre-Compact",
|
|
2035
|
+
description: "Saves session state before context compaction",
|
|
2036
|
+
version: "0.1.0",
|
|
2037
|
+
category: "Context Management",
|
|
2038
|
+
event: "Notification",
|
|
2039
|
+
matcher: "",
|
|
2040
|
+
tags: ["context", "compaction", "state", "backup"]
|
|
2041
|
+
}
|
|
2042
|
+
];
|
|
2043
|
+
});
|
|
2044
|
+
|
|
1861
2045
|
// node_modules/cli-spinners/spinners.json
|
|
1862
2046
|
var require_spinners = __commonJS((exports, module) => {
|
|
1863
2047
|
module.exports = {
|
|
@@ -3498,6 +3682,397 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
3498
3682
|
module.exports = spinners;
|
|
3499
3683
|
});
|
|
3500
3684
|
|
|
3685
|
+
// src/lib/installer.ts
|
|
3686
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3687
|
+
import { join, dirname } from "path";
|
|
3688
|
+
import { homedir } from "os";
|
|
3689
|
+
import { fileURLToPath } from "url";
|
|
3690
|
+
function getSettingsPath(scope = "global") {
|
|
3691
|
+
if (scope === "project") {
|
|
3692
|
+
return join(process.cwd(), ".claude", "settings.json");
|
|
3693
|
+
}
|
|
3694
|
+
return join(homedir(), ".claude", "settings.json");
|
|
3695
|
+
}
|
|
3696
|
+
function getHookPath(name) {
|
|
3697
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
3698
|
+
return join(HOOKS_DIR, hookName);
|
|
3699
|
+
}
|
|
3700
|
+
function hookExists(name) {
|
|
3701
|
+
return existsSync(getHookPath(name));
|
|
3702
|
+
}
|
|
3703
|
+
function readSettings(scope = "global") {
|
|
3704
|
+
const path = getSettingsPath(scope);
|
|
3705
|
+
try {
|
|
3706
|
+
if (existsSync(path)) {
|
|
3707
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
3708
|
+
}
|
|
3709
|
+
} catch {}
|
|
3710
|
+
return {};
|
|
3711
|
+
}
|
|
3712
|
+
function writeSettings(settings, scope = "global") {
|
|
3713
|
+
const path = getSettingsPath(scope);
|
|
3714
|
+
const dir = dirname(path);
|
|
3715
|
+
if (!existsSync(dir)) {
|
|
3716
|
+
mkdirSync(dir, { recursive: true });
|
|
3717
|
+
}
|
|
3718
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + `
|
|
3719
|
+
`);
|
|
3720
|
+
}
|
|
3721
|
+
function installHook(name, options = {}) {
|
|
3722
|
+
const { scope = "global", overwrite = false } = options;
|
|
3723
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
3724
|
+
const shortName = hookName.replace("hook-", "");
|
|
3725
|
+
if (!hookExists(shortName)) {
|
|
3726
|
+
return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
|
|
3727
|
+
}
|
|
3728
|
+
const registered = getRegisteredHooks(scope);
|
|
3729
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
3730
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
|
|
3731
|
+
}
|
|
3732
|
+
try {
|
|
3733
|
+
registerHook(shortName, scope);
|
|
3734
|
+
return { hook: shortName, success: true, scope };
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
return {
|
|
3737
|
+
hook: shortName,
|
|
3738
|
+
success: false,
|
|
3739
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
function registerHook(name, scope = "global") {
|
|
3744
|
+
const meta = getHook(name);
|
|
3745
|
+
if (!meta)
|
|
3746
|
+
return;
|
|
3747
|
+
const settings = readSettings(scope);
|
|
3748
|
+
if (!settings.hooks)
|
|
3749
|
+
settings.hooks = {};
|
|
3750
|
+
const eventKey = meta.event;
|
|
3751
|
+
if (!settings.hooks[eventKey])
|
|
3752
|
+
settings.hooks[eventKey] = [];
|
|
3753
|
+
const hookCommand = `hooks run ${name}`;
|
|
3754
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
|
|
3755
|
+
const entry = {
|
|
3756
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
3757
|
+
};
|
|
3758
|
+
if (meta.matcher) {
|
|
3759
|
+
entry.matcher = meta.matcher;
|
|
3760
|
+
}
|
|
3761
|
+
settings.hooks[eventKey].push(entry);
|
|
3762
|
+
writeSettings(settings, scope);
|
|
3763
|
+
}
|
|
3764
|
+
function unregisterHook(name, scope = "global") {
|
|
3765
|
+
const meta = getHook(name);
|
|
3766
|
+
if (!meta)
|
|
3767
|
+
return;
|
|
3768
|
+
const settings = readSettings(scope);
|
|
3769
|
+
if (!settings.hooks)
|
|
3770
|
+
return;
|
|
3771
|
+
const eventKey = meta.event;
|
|
3772
|
+
if (!settings.hooks[eventKey])
|
|
3773
|
+
return;
|
|
3774
|
+
const hookCommand = `hooks run ${name}`;
|
|
3775
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
|
|
3776
|
+
if (settings.hooks[eventKey].length === 0) {
|
|
3777
|
+
delete settings.hooks[eventKey];
|
|
3778
|
+
}
|
|
3779
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
3780
|
+
delete settings.hooks;
|
|
3781
|
+
}
|
|
3782
|
+
writeSettings(settings, scope);
|
|
3783
|
+
}
|
|
3784
|
+
function getRegisteredHooks(scope = "global") {
|
|
3785
|
+
const settings = readSettings(scope);
|
|
3786
|
+
if (!settings.hooks)
|
|
3787
|
+
return [];
|
|
3788
|
+
const registered = [];
|
|
3789
|
+
for (const eventKey of Object.keys(settings.hooks)) {
|
|
3790
|
+
for (const entry of settings.hooks[eventKey]) {
|
|
3791
|
+
for (const hook of entry.hooks || []) {
|
|
3792
|
+
const newMatch = hook.command?.match(/^hooks run (\w+)$/);
|
|
3793
|
+
const oldMatch = hook.command?.match(/^hook-(\w+)$/);
|
|
3794
|
+
const match = newMatch || oldMatch;
|
|
3795
|
+
if (match) {
|
|
3796
|
+
registered.push(match[1]);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return [...new Set(registered)];
|
|
3802
|
+
}
|
|
3803
|
+
function getInstalledHooks(scope = "global") {
|
|
3804
|
+
return getRegisteredHooks(scope);
|
|
3805
|
+
}
|
|
3806
|
+
function removeHook(name, scope = "global") {
|
|
3807
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
3808
|
+
const shortName = hookName.replace("hook-", "");
|
|
3809
|
+
const registered = getRegisteredHooks(scope);
|
|
3810
|
+
if (!registered.includes(shortName)) {
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
unregisterHook(shortName, scope);
|
|
3814
|
+
return true;
|
|
3815
|
+
}
|
|
3816
|
+
var __dirname2, HOOKS_DIR;
|
|
3817
|
+
var init_installer = __esm(() => {
|
|
3818
|
+
init_registry();
|
|
3819
|
+
__dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
3820
|
+
HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
// src/mcp/server.ts
|
|
3824
|
+
var exports_server = {};
|
|
3825
|
+
__export(exports_server, {
|
|
3826
|
+
startStdioServer: () => startStdioServer,
|
|
3827
|
+
startSSEServer: () => startSSEServer,
|
|
3828
|
+
createHooksServer: () => createHooksServer,
|
|
3829
|
+
MCP_PORT: () => MCP_PORT
|
|
3830
|
+
});
|
|
3831
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3832
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3833
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3834
|
+
import { z } from "zod";
|
|
3835
|
+
import { createServer } from "http";
|
|
3836
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3837
|
+
import { join as join2 } from "path";
|
|
3838
|
+
function createHooksServer() {
|
|
3839
|
+
const server = new McpServer({
|
|
3840
|
+
name: "@hasna/hooks",
|
|
3841
|
+
version: "0.0.7"
|
|
3842
|
+
});
|
|
3843
|
+
server.tool("hooks_list", "List all available Claude Code hooks, optionally filtered by category", { category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security', 'Notifications', 'Context Management')") }, async ({ category }) => {
|
|
3844
|
+
if (category) {
|
|
3845
|
+
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
3846
|
+
if (!cat) {
|
|
3847
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
|
|
3848
|
+
}
|
|
3849
|
+
return { content: [{ type: "text", text: JSON.stringify(getHooksByCategory(cat)) }] };
|
|
3850
|
+
}
|
|
3851
|
+
const result = {};
|
|
3852
|
+
for (const cat of CATEGORIES) {
|
|
3853
|
+
result[cat] = getHooksByCategory(cat);
|
|
3854
|
+
}
|
|
3855
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3856
|
+
});
|
|
3857
|
+
server.tool("hooks_search", "Search for hooks by name, description, or tags", { query: z.string().describe("Search query") }, async ({ query }) => {
|
|
3858
|
+
const results = searchHooks(query);
|
|
3859
|
+
return { content: [{ type: "text", text: JSON.stringify(results) }] };
|
|
3860
|
+
});
|
|
3861
|
+
server.tool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
|
|
3862
|
+
const meta = getHook(name);
|
|
3863
|
+
if (!meta) {
|
|
3864
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
3865
|
+
}
|
|
3866
|
+
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
3867
|
+
const projectInstalled = getRegisteredHooks("project").includes(meta.name);
|
|
3868
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }) }] };
|
|
3869
|
+
});
|
|
3870
|
+
server.tool("hooks_install", "Install one or more hooks by registering them in Claude settings. Use scope 'global' for ~/.claude/settings.json or 'project' for .claude/settings.json", {
|
|
3871
|
+
hooks: z.array(z.string()).describe("Hook names to install"),
|
|
3872
|
+
scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
3873
|
+
overwrite: z.boolean().default(false).describe("Overwrite if already installed")
|
|
3874
|
+
}, async ({ hooks, scope, overwrite }) => {
|
|
3875
|
+
const results = hooks.map((name) => installHook(name, { scope, overwrite }));
|
|
3876
|
+
return {
|
|
3877
|
+
content: [{
|
|
3878
|
+
type: "text",
|
|
3879
|
+
text: JSON.stringify({
|
|
3880
|
+
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
3881
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
3882
|
+
total: results.length,
|
|
3883
|
+
success: results.filter((r) => r.success).length,
|
|
3884
|
+
scope
|
|
3885
|
+
})
|
|
3886
|
+
}]
|
|
3887
|
+
};
|
|
3888
|
+
});
|
|
3889
|
+
server.tool("hooks_install_category", "Install all hooks in a category", {
|
|
3890
|
+
category: z.string().describe("Category name"),
|
|
3891
|
+
scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
3892
|
+
overwrite: z.boolean().default(false).describe("Overwrite if already installed")
|
|
3893
|
+
}, async ({ category, scope, overwrite }) => {
|
|
3894
|
+
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
3895
|
+
if (!cat) {
|
|
3896
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
|
|
3897
|
+
}
|
|
3898
|
+
const hooks = getHooksByCategory(cat).map((h) => h.name);
|
|
3899
|
+
const results = hooks.map((name) => installHook(name, { scope, overwrite }));
|
|
3900
|
+
return {
|
|
3901
|
+
content: [{
|
|
3902
|
+
type: "text",
|
|
3903
|
+
text: JSON.stringify({
|
|
3904
|
+
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
3905
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
3906
|
+
category: cat,
|
|
3907
|
+
scope
|
|
3908
|
+
})
|
|
3909
|
+
}]
|
|
3910
|
+
};
|
|
3911
|
+
});
|
|
3912
|
+
server.tool("hooks_install_all", "Install all available hooks", {
|
|
3913
|
+
scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
3914
|
+
overwrite: z.boolean().default(false).describe("Overwrite if already installed")
|
|
3915
|
+
}, async ({ scope, overwrite }) => {
|
|
3916
|
+
const results = HOOKS.map((h) => installHook(h.name, { scope, overwrite }));
|
|
3917
|
+
return {
|
|
3918
|
+
content: [{
|
|
3919
|
+
type: "text",
|
|
3920
|
+
text: JSON.stringify({
|
|
3921
|
+
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
3922
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
3923
|
+
total: results.length,
|
|
3924
|
+
success: results.filter((r) => r.success).length,
|
|
3925
|
+
scope
|
|
3926
|
+
})
|
|
3927
|
+
}]
|
|
3928
|
+
};
|
|
3929
|
+
});
|
|
3930
|
+
server.tool("hooks_remove", "Remove (unregister) a hook from Claude settings", {
|
|
3931
|
+
name: z.string().describe("Hook name to remove"),
|
|
3932
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to remove from")
|
|
3933
|
+
}, async ({ name, scope }) => {
|
|
3934
|
+
const removed = removeHook(name, scope);
|
|
3935
|
+
return { content: [{ type: "text", text: JSON.stringify({ hook: name, removed, scope }) }] };
|
|
3936
|
+
});
|
|
3937
|
+
server.tool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
|
|
3938
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
|
|
3939
|
+
}, async ({ scope }) => {
|
|
3940
|
+
const settingsPath = getSettingsPath(scope);
|
|
3941
|
+
const issues = [];
|
|
3942
|
+
const healthy = [];
|
|
3943
|
+
const settingsExist = existsSync2(settingsPath);
|
|
3944
|
+
if (!settingsExist) {
|
|
3945
|
+
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
3946
|
+
}
|
|
3947
|
+
const registered = getRegisteredHooks(scope);
|
|
3948
|
+
for (const name of registered) {
|
|
3949
|
+
const meta = getHook(name);
|
|
3950
|
+
let hookHealthy = true;
|
|
3951
|
+
if (!hookExists(name)) {
|
|
3952
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
const hookDir = getHookPath(name);
|
|
3956
|
+
if (!existsSync2(join2(hookDir, "src", "hook.ts"))) {
|
|
3957
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
3958
|
+
hookHealthy = false;
|
|
3959
|
+
}
|
|
3960
|
+
if (meta && settingsExist) {
|
|
3961
|
+
try {
|
|
3962
|
+
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
3963
|
+
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
3964
|
+
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
|
|
3965
|
+
if (!found) {
|
|
3966
|
+
issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
|
|
3967
|
+
hookHealthy = false;
|
|
3968
|
+
}
|
|
3969
|
+
} catch {}
|
|
3970
|
+
}
|
|
3971
|
+
if (hookHealthy)
|
|
3972
|
+
healthy.push(name);
|
|
3973
|
+
}
|
|
3974
|
+
return { content: [{ type: "text", text: JSON.stringify({ healthy, issues, registered, scope }) }] };
|
|
3975
|
+
});
|
|
3976
|
+
server.tool("hooks_categories", "List all hook categories with counts", {}, async () => {
|
|
3977
|
+
const result = CATEGORIES.map((cat) => ({
|
|
3978
|
+
name: cat,
|
|
3979
|
+
count: getHooksByCategory(cat).length
|
|
3980
|
+
}));
|
|
3981
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3982
|
+
});
|
|
3983
|
+
server.tool("hooks_docs", "Get documentation \u2014 general overview or README for a specific hook", { name: z.string().optional().describe("Hook name for specific docs, omit for general docs") }, async ({ name }) => {
|
|
3984
|
+
if (name) {
|
|
3985
|
+
const meta = getHook(name);
|
|
3986
|
+
if (!meta) {
|
|
3987
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
3988
|
+
}
|
|
3989
|
+
const hookPath = getHookPath(name);
|
|
3990
|
+
const readmePath = join2(hookPath, "README.md");
|
|
3991
|
+
let readme = "";
|
|
3992
|
+
if (existsSync2(readmePath)) {
|
|
3993
|
+
readme = readFileSync2(readmePath, "utf-8");
|
|
3994
|
+
}
|
|
3995
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
3996
|
+
}
|
|
3997
|
+
return {
|
|
3998
|
+
content: [{
|
|
3999
|
+
type: "text",
|
|
4000
|
+
text: JSON.stringify({
|
|
4001
|
+
overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session. Install @hasna/hooks globally, then register hooks \u2014 no files are copied to your project.",
|
|
4002
|
+
events: {
|
|
4003
|
+
PreToolUse: "Fires before a tool executes. Can block the operation.",
|
|
4004
|
+
PostToolUse: "Fires after a tool executes. Runs asynchronously.",
|
|
4005
|
+
Stop: "Fires when a session ends. Useful for notifications.",
|
|
4006
|
+
Notification: "Fires on notification events like context compaction."
|
|
4007
|
+
},
|
|
4008
|
+
commands: {
|
|
4009
|
+
install: "hooks install <name>",
|
|
4010
|
+
installProject: "hooks install <name> --project",
|
|
4011
|
+
installAll: "hooks install --all",
|
|
4012
|
+
remove: "hooks remove <name>",
|
|
4013
|
+
list: "hooks list",
|
|
4014
|
+
search: "hooks search <query>",
|
|
4015
|
+
doctor: "hooks doctor"
|
|
4016
|
+
}
|
|
4017
|
+
})
|
|
4018
|
+
}]
|
|
4019
|
+
};
|
|
4020
|
+
});
|
|
4021
|
+
server.tool("hooks_registered", "Get list of currently registered hooks for a scope", {
|
|
4022
|
+
scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
|
|
4023
|
+
}, async ({ scope }) => {
|
|
4024
|
+
const registered = getRegisteredHooks(scope);
|
|
4025
|
+
const result = registered.map((name) => {
|
|
4026
|
+
const meta = getHook(name);
|
|
4027
|
+
return { name, event: meta?.event, version: meta?.version, description: meta?.description };
|
|
4028
|
+
});
|
|
4029
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4030
|
+
});
|
|
4031
|
+
return server;
|
|
4032
|
+
}
|
|
4033
|
+
async function startSSEServer(port = MCP_PORT) {
|
|
4034
|
+
const server = createHooksServer();
|
|
4035
|
+
const transports = new Map;
|
|
4036
|
+
const httpServer = createServer(async (req, res) => {
|
|
4037
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
4038
|
+
if (url.pathname === "/sse") {
|
|
4039
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
4040
|
+
transports.set(transport.sessionId, transport);
|
|
4041
|
+
res.on("close", () => transports.delete(transport.sessionId));
|
|
4042
|
+
await server.connect(transport);
|
|
4043
|
+
} else if (url.pathname === "/messages") {
|
|
4044
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
4045
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
4046
|
+
res.writeHead(400);
|
|
4047
|
+
res.end("Invalid session");
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
const transport = transports.get(sessionId);
|
|
4051
|
+
let body = "";
|
|
4052
|
+
for await (const chunk of req)
|
|
4053
|
+
body += chunk;
|
|
4054
|
+
await transport.handlePostMessage(req, res, body);
|
|
4055
|
+
} else {
|
|
4056
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4057
|
+
res.end(JSON.stringify({ name: "@hasna/hooks", version: "0.0.7", transport: "sse", port }));
|
|
4058
|
+
}
|
|
4059
|
+
});
|
|
4060
|
+
httpServer.listen(port, () => {
|
|
4061
|
+
console.error(`@hasna/hooks MCP server running on http://localhost:${port}`);
|
|
4062
|
+
console.error(`SSE endpoint: http://localhost:${port}/sse`);
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
async function startStdioServer() {
|
|
4066
|
+
const server = createHooksServer();
|
|
4067
|
+
const transport = new StdioServerTransport;
|
|
4068
|
+
await server.connect(transport);
|
|
4069
|
+
}
|
|
4070
|
+
var MCP_PORT = 39427;
|
|
4071
|
+
var init_server = __esm(() => {
|
|
4072
|
+
init_registry();
|
|
4073
|
+
init_installer();
|
|
4074
|
+
});
|
|
4075
|
+
|
|
3501
4076
|
// src/cli/index.tsx
|
|
3502
4077
|
import { render } from "ink";
|
|
3503
4078
|
|
|
@@ -3519,9 +4094,8 @@ var {
|
|
|
3519
4094
|
|
|
3520
4095
|
// src/cli/index.tsx
|
|
3521
4096
|
import chalk2 from "chalk";
|
|
3522
|
-
import { existsSync as
|
|
3523
|
-
import { join as
|
|
3524
|
-
import { homedir as homedir2 } from "os";
|
|
4097
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4098
|
+
import { join as join3 } from "path";
|
|
3525
4099
|
|
|
3526
4100
|
// src/cli/components/App.tsx
|
|
3527
4101
|
import { useState as useState7 } from "react";
|
|
@@ -3953,179 +4527,7 @@ function Header({ title = "Hooks", subtitle }) {
|
|
|
3953
4527
|
|
|
3954
4528
|
// src/cli/components/CategorySelect.tsx
|
|
3955
4529
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
3956
|
-
|
|
3957
|
-
// src/lib/registry.ts
|
|
3958
|
-
var CATEGORIES = [
|
|
3959
|
-
"Git Safety",
|
|
3960
|
-
"Code Quality",
|
|
3961
|
-
"Security",
|
|
3962
|
-
"Notifications",
|
|
3963
|
-
"Context Management"
|
|
3964
|
-
];
|
|
3965
|
-
var HOOKS = [
|
|
3966
|
-
{
|
|
3967
|
-
name: "gitguard",
|
|
3968
|
-
displayName: "Git Guard",
|
|
3969
|
-
description: "Blocks destructive git operations like reset --hard, push --force, clean -f",
|
|
3970
|
-
version: "0.1.0",
|
|
3971
|
-
category: "Git Safety",
|
|
3972
|
-
event: "PreToolUse",
|
|
3973
|
-
matcher: "Bash",
|
|
3974
|
-
tags: ["git", "safety", "destructive", "guard"]
|
|
3975
|
-
},
|
|
3976
|
-
{
|
|
3977
|
-
name: "branchprotect",
|
|
3978
|
-
displayName: "Branch Protect",
|
|
3979
|
-
description: "Prevents editing files directly on main/master branch",
|
|
3980
|
-
version: "0.1.0",
|
|
3981
|
-
category: "Git Safety",
|
|
3982
|
-
event: "PreToolUse",
|
|
3983
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
3984
|
-
tags: ["git", "branch", "protection", "main"]
|
|
3985
|
-
},
|
|
3986
|
-
{
|
|
3987
|
-
name: "checkpoint",
|
|
3988
|
-
displayName: "Checkpoint",
|
|
3989
|
-
description: "Creates shadow git snapshots before file modifications for easy rollback",
|
|
3990
|
-
version: "0.1.0",
|
|
3991
|
-
category: "Git Safety",
|
|
3992
|
-
event: "PreToolUse",
|
|
3993
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
3994
|
-
tags: ["git", "snapshot", "rollback", "backup"]
|
|
3995
|
-
},
|
|
3996
|
-
{
|
|
3997
|
-
name: "checktests",
|
|
3998
|
-
displayName: "Check Tests",
|
|
3999
|
-
description: "Checks for missing tests after file edits",
|
|
4000
|
-
version: "0.1.6",
|
|
4001
|
-
category: "Code Quality",
|
|
4002
|
-
event: "PostToolUse",
|
|
4003
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4004
|
-
tags: ["tests", "coverage", "quality"]
|
|
4005
|
-
},
|
|
4006
|
-
{
|
|
4007
|
-
name: "checklint",
|
|
4008
|
-
displayName: "Check Lint",
|
|
4009
|
-
description: "Runs linting after file edits and creates tasks for errors",
|
|
4010
|
-
version: "0.1.7",
|
|
4011
|
-
category: "Code Quality",
|
|
4012
|
-
event: "PostToolUse",
|
|
4013
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4014
|
-
tags: ["lint", "style", "quality"]
|
|
4015
|
-
},
|
|
4016
|
-
{
|
|
4017
|
-
name: "checkfiles",
|
|
4018
|
-
displayName: "Check Files",
|
|
4019
|
-
description: "Runs headless agent to review files and create tasks",
|
|
4020
|
-
version: "0.1.4",
|
|
4021
|
-
category: "Code Quality",
|
|
4022
|
-
event: "PostToolUse",
|
|
4023
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4024
|
-
tags: ["review", "files", "quality"]
|
|
4025
|
-
},
|
|
4026
|
-
{
|
|
4027
|
-
name: "checkbugs",
|
|
4028
|
-
displayName: "Check Bugs",
|
|
4029
|
-
description: "Checks for bugs via Codex headless agent",
|
|
4030
|
-
version: "0.1.6",
|
|
4031
|
-
category: "Code Quality",
|
|
4032
|
-
event: "PostToolUse",
|
|
4033
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4034
|
-
tags: ["bugs", "analysis", "quality"]
|
|
4035
|
-
},
|
|
4036
|
-
{
|
|
4037
|
-
name: "checkdocs",
|
|
4038
|
-
displayName: "Check Docs",
|
|
4039
|
-
description: "Checks for missing documentation and creates tasks",
|
|
4040
|
-
version: "0.2.1",
|
|
4041
|
-
category: "Code Quality",
|
|
4042
|
-
event: "PostToolUse",
|
|
4043
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4044
|
-
tags: ["docs", "documentation", "quality"]
|
|
4045
|
-
},
|
|
4046
|
-
{
|
|
4047
|
-
name: "checktasks",
|
|
4048
|
-
displayName: "Check Tasks",
|
|
4049
|
-
description: "Validates task completion and tracks progress",
|
|
4050
|
-
version: "1.0.8",
|
|
4051
|
-
category: "Code Quality",
|
|
4052
|
-
event: "PostToolUse",
|
|
4053
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4054
|
-
tags: ["tasks", "tracking", "quality"]
|
|
4055
|
-
},
|
|
4056
|
-
{
|
|
4057
|
-
name: "checksecurity",
|
|
4058
|
-
displayName: "Check Security",
|
|
4059
|
-
description: "Runs security checks via Claude and Codex headless agents",
|
|
4060
|
-
version: "0.1.6",
|
|
4061
|
-
category: "Security",
|
|
4062
|
-
event: "PostToolUse",
|
|
4063
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4064
|
-
tags: ["security", "audit", "vulnerabilities"]
|
|
4065
|
-
},
|
|
4066
|
-
{
|
|
4067
|
-
name: "packageage",
|
|
4068
|
-
displayName: "Package Age",
|
|
4069
|
-
description: "Checks package age before install to prevent typosquatting",
|
|
4070
|
-
version: "0.1.1",
|
|
4071
|
-
category: "Security",
|
|
4072
|
-
event: "PreToolUse",
|
|
4073
|
-
matcher: "Bash",
|
|
4074
|
-
tags: ["npm", "packages", "typosquatting", "supply-chain"]
|
|
4075
|
-
},
|
|
4076
|
-
{
|
|
4077
|
-
name: "phonenotify",
|
|
4078
|
-
displayName: "Phone Notify",
|
|
4079
|
-
description: "Sends push notifications to phone via ntfy.sh",
|
|
4080
|
-
version: "0.1.0",
|
|
4081
|
-
category: "Notifications",
|
|
4082
|
-
event: "Stop",
|
|
4083
|
-
matcher: "",
|
|
4084
|
-
tags: ["notification", "phone", "push", "ntfy"]
|
|
4085
|
-
},
|
|
4086
|
-
{
|
|
4087
|
-
name: "agentmessages",
|
|
4088
|
-
displayName: "Agent Messages",
|
|
4089
|
-
description: "Inter-agent messaging integration for service-message",
|
|
4090
|
-
version: "0.1.0",
|
|
4091
|
-
category: "Notifications",
|
|
4092
|
-
event: "Stop",
|
|
4093
|
-
matcher: "",
|
|
4094
|
-
tags: ["messaging", "agents", "inter-agent"]
|
|
4095
|
-
},
|
|
4096
|
-
{
|
|
4097
|
-
name: "contextrefresh",
|
|
4098
|
-
displayName: "Context Refresh",
|
|
4099
|
-
description: "Re-injects important context every N prompts to prevent drift",
|
|
4100
|
-
version: "0.1.0",
|
|
4101
|
-
category: "Context Management",
|
|
4102
|
-
event: "Notification",
|
|
4103
|
-
matcher: "",
|
|
4104
|
-
tags: ["context", "memory", "prompts", "refresh"]
|
|
4105
|
-
},
|
|
4106
|
-
{
|
|
4107
|
-
name: "precompact",
|
|
4108
|
-
displayName: "Pre-Compact",
|
|
4109
|
-
description: "Saves session state before context compaction",
|
|
4110
|
-
version: "0.1.0",
|
|
4111
|
-
category: "Context Management",
|
|
4112
|
-
event: "Notification",
|
|
4113
|
-
matcher: "",
|
|
4114
|
-
tags: ["context", "compaction", "state", "backup"]
|
|
4115
|
-
}
|
|
4116
|
-
];
|
|
4117
|
-
function getHooksByCategory(category) {
|
|
4118
|
-
return HOOKS.filter((h) => h.category === category);
|
|
4119
|
-
}
|
|
4120
|
-
function searchHooks(query) {
|
|
4121
|
-
const q = query.toLowerCase();
|
|
4122
|
-
return HOOKS.filter((h) => h.name.toLowerCase().includes(q) || h.displayName.toLowerCase().includes(q) || h.description.toLowerCase().includes(q) || h.tags.some((t) => t.includes(q)));
|
|
4123
|
-
}
|
|
4124
|
-
function getHook(name) {
|
|
4125
|
-
return HOOKS.find((h) => h.name === name);
|
|
4126
|
-
}
|
|
4127
|
-
|
|
4128
|
-
// src/cli/components/CategorySelect.tsx
|
|
4530
|
+
init_registry();
|
|
4129
4531
|
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
4130
4532
|
function CategorySelect({ onSelect, onBack }) {
|
|
4131
4533
|
const items = CATEGORIES.map((cat) => ({
|
|
@@ -4415,6 +4817,7 @@ function TextInput({ value: originalValue, placeholder = "", focus = true, mask,
|
|
|
4415
4817
|
var build_default = TextInput;
|
|
4416
4818
|
|
|
4417
4819
|
// src/cli/components/SearchView.tsx
|
|
4820
|
+
init_registry();
|
|
4418
4821
|
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
4419
4822
|
function SearchView({
|
|
4420
4823
|
selected,
|
|
@@ -4521,185 +4924,9 @@ function Spinner({ type = "dots" }) {
|
|
|
4521
4924
|
}
|
|
4522
4925
|
var build_default2 = Spinner;
|
|
4523
4926
|
|
|
4524
|
-
// src/lib/installer.ts
|
|
4525
|
-
import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4526
|
-
import { join, dirname } from "path";
|
|
4527
|
-
import { homedir } from "os";
|
|
4528
|
-
import { fileURLToPath } from "url";
|
|
4529
|
-
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
4530
|
-
var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
|
|
4531
|
-
var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
4532
|
-
function getHookPath(name) {
|
|
4533
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4534
|
-
return join(HOOKS_DIR, hookName);
|
|
4535
|
-
}
|
|
4536
|
-
function readSettings() {
|
|
4537
|
-
try {
|
|
4538
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
4539
|
-
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
4540
|
-
}
|
|
4541
|
-
} catch {}
|
|
4542
|
-
return {};
|
|
4543
|
-
}
|
|
4544
|
-
function writeSettings(settings) {
|
|
4545
|
-
const dir = dirname(SETTINGS_PATH);
|
|
4546
|
-
if (!existsSync(dir)) {
|
|
4547
|
-
mkdirSync(dir, { recursive: true });
|
|
4548
|
-
}
|
|
4549
|
-
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
|
|
4550
|
-
`);
|
|
4551
|
-
}
|
|
4552
|
-
function installHook(name, options = {}) {
|
|
4553
|
-
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
4554
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4555
|
-
const shortName = hookName.replace("hook-", "");
|
|
4556
|
-
const sourcePath = getHookPath(name);
|
|
4557
|
-
const destDir = join(targetDir, ".hooks");
|
|
4558
|
-
const destPath = join(destDir, hookName);
|
|
4559
|
-
if (!existsSync(sourcePath)) {
|
|
4560
|
-
return {
|
|
4561
|
-
hook: shortName,
|
|
4562
|
-
success: false,
|
|
4563
|
-
error: `Hook '${shortName}' not found`
|
|
4564
|
-
};
|
|
4565
|
-
}
|
|
4566
|
-
if (existsSync(destPath) && !overwrite) {
|
|
4567
|
-
return {
|
|
4568
|
-
hook: shortName,
|
|
4569
|
-
success: false,
|
|
4570
|
-
error: `Already installed. Use --overwrite to replace.`,
|
|
4571
|
-
path: destPath
|
|
4572
|
-
};
|
|
4573
|
-
}
|
|
4574
|
-
try {
|
|
4575
|
-
if (!existsSync(destDir)) {
|
|
4576
|
-
mkdirSync(destDir, { recursive: true });
|
|
4577
|
-
}
|
|
4578
|
-
if (overwrite && existsSync(destPath)) {
|
|
4579
|
-
const { rmSync } = __require("fs");
|
|
4580
|
-
rmSync(destPath, { recursive: true, force: true });
|
|
4581
|
-
}
|
|
4582
|
-
cpSync(sourcePath, destPath, { recursive: true });
|
|
4583
|
-
registerHookInSettings(shortName);
|
|
4584
|
-
updateHooksIndex(destDir);
|
|
4585
|
-
return {
|
|
4586
|
-
hook: shortName,
|
|
4587
|
-
success: true,
|
|
4588
|
-
path: destPath
|
|
4589
|
-
};
|
|
4590
|
-
} catch (error) {
|
|
4591
|
-
return {
|
|
4592
|
-
hook: shortName,
|
|
4593
|
-
success: false,
|
|
4594
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
4595
|
-
};
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
function registerHookInSettings(name) {
|
|
4599
|
-
const meta = getHook(name);
|
|
4600
|
-
if (!meta)
|
|
4601
|
-
return;
|
|
4602
|
-
const settings = readSettings();
|
|
4603
|
-
if (!settings.hooks)
|
|
4604
|
-
settings.hooks = {};
|
|
4605
|
-
const eventKey = meta.event;
|
|
4606
|
-
if (!settings.hooks[eventKey])
|
|
4607
|
-
settings.hooks[eventKey] = [];
|
|
4608
|
-
const hookCommand = `hook-${name}`;
|
|
4609
|
-
const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
|
|
4610
|
-
if (existing)
|
|
4611
|
-
return;
|
|
4612
|
-
const entry = {
|
|
4613
|
-
hooks: [{ type: "command", command: hookCommand }]
|
|
4614
|
-
};
|
|
4615
|
-
if (meta.matcher) {
|
|
4616
|
-
entry.matcher = meta.matcher;
|
|
4617
|
-
}
|
|
4618
|
-
settings.hooks[eventKey].push(entry);
|
|
4619
|
-
writeSettings(settings);
|
|
4620
|
-
}
|
|
4621
|
-
function unregisterHookFromSettings(name) {
|
|
4622
|
-
const meta = getHook(name);
|
|
4623
|
-
if (!meta)
|
|
4624
|
-
return;
|
|
4625
|
-
const settings = readSettings();
|
|
4626
|
-
if (!settings.hooks)
|
|
4627
|
-
return;
|
|
4628
|
-
const eventKey = meta.event;
|
|
4629
|
-
if (!settings.hooks[eventKey])
|
|
4630
|
-
return;
|
|
4631
|
-
const hookCommand = `hook-${name}`;
|
|
4632
|
-
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
|
|
4633
|
-
if (settings.hooks[eventKey].length === 0) {
|
|
4634
|
-
delete settings.hooks[eventKey];
|
|
4635
|
-
}
|
|
4636
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
4637
|
-
delete settings.hooks;
|
|
4638
|
-
}
|
|
4639
|
-
writeSettings(settings);
|
|
4640
|
-
}
|
|
4641
|
-
function updateHooksIndex(hooksDir) {
|
|
4642
|
-
const indexPath = join(hooksDir, "index.ts");
|
|
4643
|
-
const { readdirSync } = __require("fs");
|
|
4644
|
-
const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
|
|
4645
|
-
const exports = hooks.map((h) => {
|
|
4646
|
-
const name = h.replace("hook-", "");
|
|
4647
|
-
return `export * as ${name} from './${h}/src/index.js';`;
|
|
4648
|
-
}).join(`
|
|
4649
|
-
`);
|
|
4650
|
-
const content = `/**
|
|
4651
|
-
* Auto-generated index of installed hooks
|
|
4652
|
-
* Do not edit manually - run 'hooks install' to update
|
|
4653
|
-
*/
|
|
4654
|
-
|
|
4655
|
-
${exports}
|
|
4656
|
-
`;
|
|
4657
|
-
writeFileSync(indexPath, content);
|
|
4658
|
-
}
|
|
4659
|
-
function getInstalledHooks(targetDir = process.cwd()) {
|
|
4660
|
-
const hooksDir = join(targetDir, ".hooks");
|
|
4661
|
-
if (!existsSync(hooksDir)) {
|
|
4662
|
-
return [];
|
|
4663
|
-
}
|
|
4664
|
-
const { readdirSync, statSync } = __require("fs");
|
|
4665
|
-
return readdirSync(hooksDir).filter((f) => {
|
|
4666
|
-
const fullPath = join(hooksDir, f);
|
|
4667
|
-
return f.startsWith("hook-") && statSync(fullPath).isDirectory();
|
|
4668
|
-
}).map((f) => f.replace("hook-", ""));
|
|
4669
|
-
}
|
|
4670
|
-
function getRegisteredHooks() {
|
|
4671
|
-
const settings = readSettings();
|
|
4672
|
-
if (!settings.hooks)
|
|
4673
|
-
return [];
|
|
4674
|
-
const registered = [];
|
|
4675
|
-
for (const eventKey of Object.keys(settings.hooks)) {
|
|
4676
|
-
for (const entry of settings.hooks[eventKey]) {
|
|
4677
|
-
for (const hook of entry.hooks || []) {
|
|
4678
|
-
const match = hook.command?.match(/hook-(\w+)/);
|
|
4679
|
-
if (match) {
|
|
4680
|
-
registered.push(match[1]);
|
|
4681
|
-
}
|
|
4682
|
-
}
|
|
4683
|
-
}
|
|
4684
|
-
}
|
|
4685
|
-
return [...new Set(registered)];
|
|
4686
|
-
}
|
|
4687
|
-
function removeHook(name, targetDir = process.cwd()) {
|
|
4688
|
-
const { rmSync } = __require("fs");
|
|
4689
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4690
|
-
const shortName = hookName.replace("hook-", "");
|
|
4691
|
-
const hooksDir = join(targetDir, ".hooks");
|
|
4692
|
-
const hookPath = join(hooksDir, hookName);
|
|
4693
|
-
if (!existsSync(hookPath)) {
|
|
4694
|
-
return false;
|
|
4695
|
-
}
|
|
4696
|
-
rmSync(hookPath, { recursive: true });
|
|
4697
|
-
unregisterHookFromSettings(shortName);
|
|
4698
|
-
updateHooksIndex(hooksDir);
|
|
4699
|
-
return true;
|
|
4700
|
-
}
|
|
4701
|
-
|
|
4702
4927
|
// src/cli/components/InstallProgress.tsx
|
|
4928
|
+
init_installer();
|
|
4929
|
+
init_registry();
|
|
4703
4930
|
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
4704
4931
|
function InstallProgress({
|
|
4705
4932
|
hooks,
|
|
@@ -4827,6 +5054,7 @@ function InstallProgress({
|
|
|
4827
5054
|
}
|
|
4828
5055
|
|
|
4829
5056
|
// src/cli/components/App.tsx
|
|
5057
|
+
init_registry();
|
|
4830
5058
|
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
4831
5059
|
function App({ initialHooks, overwrite = false }) {
|
|
4832
5060
|
const { exit } = useApp();
|
|
@@ -5033,13 +5261,49 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
5033
5261
|
}
|
|
5034
5262
|
|
|
5035
5263
|
// src/cli/index.tsx
|
|
5264
|
+
init_registry();
|
|
5265
|
+
init_installer();
|
|
5036
5266
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5037
5267
|
var program2 = new Command;
|
|
5038
|
-
|
|
5268
|
+
function resolveScope(options) {
|
|
5269
|
+
if (options.project)
|
|
5270
|
+
return "project";
|
|
5271
|
+
return "global";
|
|
5272
|
+
}
|
|
5273
|
+
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.7");
|
|
5039
5274
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
5040
5275
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5041
5276
|
});
|
|
5042
|
-
program2.command("
|
|
5277
|
+
program2.command("run").argument("<hook>", "Hook to run").description("Execute a hook (called by Claude Code)").action(async (hook) => {
|
|
5278
|
+
const meta = getHook(hook);
|
|
5279
|
+
if (!meta) {
|
|
5280
|
+
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5281
|
+
process.exit(1);
|
|
5282
|
+
}
|
|
5283
|
+
const hookDir = getHookPath(hook);
|
|
5284
|
+
const hookScript = join3(hookDir, "src", "hook.ts");
|
|
5285
|
+
if (!existsSync3(hookScript)) {
|
|
5286
|
+
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5287
|
+
process.exit(1);
|
|
5288
|
+
}
|
|
5289
|
+
const stdin = await new Response(Bun.stdin.stream()).text();
|
|
5290
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
5291
|
+
stdin: new Response(stdin),
|
|
5292
|
+
stdout: "pipe",
|
|
5293
|
+
stderr: "pipe",
|
|
5294
|
+
env: process.env
|
|
5295
|
+
});
|
|
5296
|
+
const stdout = await new Response(proc.stdout).text();
|
|
5297
|
+
const stderr = await new Response(proc.stderr).text();
|
|
5298
|
+
const exitCode = await proc.exited;
|
|
5299
|
+
if (stdout)
|
|
5300
|
+
process.stdout.write(stdout);
|
|
5301
|
+
if (stderr)
|
|
5302
|
+
process.stderr.write(stderr);
|
|
5303
|
+
process.exit(exitCode);
|
|
5304
|
+
});
|
|
5305
|
+
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) => {
|
|
5306
|
+
const scope = resolveScope(options);
|
|
5043
5307
|
let toInstall = hooks;
|
|
5044
5308
|
if (options.all) {
|
|
5045
5309
|
toInstall = HOOKS.map((h) => h.name);
|
|
@@ -5062,7 +5326,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
5062
5326
|
}
|
|
5063
5327
|
const results = [];
|
|
5064
5328
|
for (const name of toInstall) {
|
|
5065
|
-
const result = installHook(name, { overwrite: options.overwrite });
|
|
5329
|
+
const result = installHook(name, { scope, overwrite: options.overwrite });
|
|
5066
5330
|
results.push(result);
|
|
5067
5331
|
}
|
|
5068
5332
|
if (options.json) {
|
|
@@ -5070,43 +5334,46 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
5070
5334
|
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
5071
5335
|
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
5072
5336
|
total: results.length,
|
|
5073
|
-
success: results.filter((r) => r.success).length
|
|
5337
|
+
success: results.filter((r) => r.success).length,
|
|
5338
|
+
scope
|
|
5074
5339
|
}));
|
|
5075
5340
|
return;
|
|
5076
5341
|
}
|
|
5342
|
+
const settingsFile = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
|
|
5077
5343
|
console.log(chalk2.bold(`
|
|
5078
|
-
Installing hooks...
|
|
5344
|
+
Installing hooks (${scope})...
|
|
5079
5345
|
`));
|
|
5080
5346
|
for (const result of results) {
|
|
5081
5347
|
if (result.success) {
|
|
5082
5348
|
const meta = getHook(result.hook);
|
|
5083
5349
|
console.log(chalk2.green(`\u2713 ${result.hook}`));
|
|
5084
5350
|
if (meta) {
|
|
5085
|
-
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192
|
|
5351
|
+
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hooks run ${result.hook}`));
|
|
5086
5352
|
}
|
|
5087
5353
|
} else {
|
|
5088
5354
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
5089
5355
|
}
|
|
5090
5356
|
}
|
|
5091
5357
|
console.log(chalk2.dim(`
|
|
5092
|
-
|
|
5358
|
+
Registered in ${settingsFile}`));
|
|
5093
5359
|
});
|
|
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) => {
|
|
5095
|
-
|
|
5096
|
-
|
|
5360
|
+
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("-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) => {
|
|
5361
|
+
const scope = resolveScope(options);
|
|
5362
|
+
if (options.registered || options.installed) {
|
|
5363
|
+
const registered = getRegisteredHooks(scope);
|
|
5097
5364
|
if (options.json) {
|
|
5098
5365
|
console.log(JSON.stringify(registered.map((name) => {
|
|
5099
5366
|
const meta = getHook(name);
|
|
5100
|
-
return { name, event: meta?.event, description: meta?.description };
|
|
5367
|
+
return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope };
|
|
5101
5368
|
})));
|
|
5102
5369
|
return;
|
|
5103
5370
|
}
|
|
5104
5371
|
if (registered.length === 0) {
|
|
5105
|
-
console.log(chalk2.dim(
|
|
5372
|
+
console.log(chalk2.dim(`No hooks registered (${scope})`));
|
|
5106
5373
|
return;
|
|
5107
5374
|
}
|
|
5108
5375
|
console.log(chalk2.bold(`
|
|
5109
|
-
Registered hooks (${registered.length}):
|
|
5376
|
+
Registered hooks \u2014 ${scope} (${registered.length}):
|
|
5110
5377
|
`));
|
|
5111
5378
|
for (const name of registered) {
|
|
5112
5379
|
const meta = getHook(name);
|
|
@@ -5114,27 +5381,6 @@ Registered hooks (${registered.length}):
|
|
|
5114
5381
|
}
|
|
5115
5382
|
return;
|
|
5116
5383
|
}
|
|
5117
|
-
if (options.installed) {
|
|
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
|
-
}
|
|
5126
|
-
if (installed.length === 0) {
|
|
5127
|
-
console.log(chalk2.dim("No hooks installed"));
|
|
5128
|
-
return;
|
|
5129
|
-
}
|
|
5130
|
-
console.log(chalk2.bold(`
|
|
5131
|
-
Installed hooks (${installed.length}):
|
|
5132
|
-
`));
|
|
5133
|
-
for (const name of installed) {
|
|
5134
|
-
console.log(` ${name}`);
|
|
5135
|
-
}
|
|
5136
|
-
return;
|
|
5137
|
-
}
|
|
5138
5384
|
if (options.category) {
|
|
5139
5385
|
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
5140
5386
|
if (!category) {
|
|
@@ -5197,16 +5443,17 @@ Found ${results.length} hook(s):
|
|
|
5197
5443
|
console.log(` ${h.description}`);
|
|
5198
5444
|
}
|
|
5199
5445
|
});
|
|
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) => {
|
|
5201
|
-
const
|
|
5446
|
+
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) => {
|
|
5447
|
+
const scope = resolveScope(options);
|
|
5448
|
+
const removed = removeHook(hook, scope);
|
|
5202
5449
|
if (options.json) {
|
|
5203
|
-
console.log(JSON.stringify({ hook, removed }));
|
|
5450
|
+
console.log(JSON.stringify({ hook, removed, scope }));
|
|
5204
5451
|
return;
|
|
5205
5452
|
}
|
|
5206
5453
|
if (removed) {
|
|
5207
|
-
console.log(chalk2.green(`\u2713 Removed ${hook} (
|
|
5454
|
+
console.log(chalk2.green(`\u2713 Removed ${hook} (${scope})`));
|
|
5208
5455
|
} else {
|
|
5209
|
-
console.log(chalk2.red(`\u2717 ${hook} is not installed`));
|
|
5456
|
+
console.log(chalk2.red(`\u2717 ${hook} is not installed (${scope})`));
|
|
5210
5457
|
}
|
|
5211
5458
|
});
|
|
5212
5459
|
program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
@@ -5236,12 +5483,10 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
5236
5483
|
}
|
|
5237
5484
|
return;
|
|
5238
5485
|
}
|
|
5239
|
-
const
|
|
5240
|
-
const
|
|
5241
|
-
const installed = getInstalledHooks();
|
|
5242
|
-
const isInstalled = installed.includes(meta.name);
|
|
5486
|
+
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
5487
|
+
const projectInstalled = getRegisteredHooks("project").includes(meta.name);
|
|
5243
5488
|
if (options.json) {
|
|
5244
|
-
console.log(JSON.stringify({ ...meta,
|
|
5489
|
+
console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
|
|
5245
5490
|
return;
|
|
5246
5491
|
}
|
|
5247
5492
|
console.log(chalk2.bold(`
|
|
@@ -5254,53 +5499,50 @@ ${meta.displayName}
|
|
|
5254
5499
|
console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
|
|
5255
5500
|
console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
5256
5501
|
console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
5257
|
-
console.log(` ${chalk2.dim("
|
|
5502
|
+
console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
|
|
5258
5503
|
console.log();
|
|
5259
|
-
if (
|
|
5260
|
-
console.log(chalk2.green(" \u25CF
|
|
5504
|
+
if (globalInstalled) {
|
|
5505
|
+
console.log(chalk2.green(" \u25CF Installed globally"));
|
|
5261
5506
|
} else {
|
|
5262
|
-
console.log(chalk2.dim(" \u25CB Not
|
|
5507
|
+
console.log(chalk2.dim(" \u25CB Not installed globally"));
|
|
5263
5508
|
}
|
|
5264
|
-
if (
|
|
5265
|
-
console.log(chalk2.green(" \u25CF Installed in
|
|
5509
|
+
if (projectInstalled) {
|
|
5510
|
+
console.log(chalk2.green(" \u25CF Installed in project"));
|
|
5266
5511
|
} else {
|
|
5267
|
-
console.log(chalk2.dim(" \u25CB Not installed"));
|
|
5512
|
+
console.log(chalk2.dim(" \u25CB Not installed in project"));
|
|
5268
5513
|
}
|
|
5269
5514
|
});
|
|
5270
|
-
program2.command("doctor").option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
|
|
5271
|
-
const
|
|
5515
|
+
program2.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
|
|
5516
|
+
const scope = resolveScope(options);
|
|
5517
|
+
const settingsPath = getSettingsPath(scope);
|
|
5272
5518
|
const issues = [];
|
|
5273
5519
|
const healthy = [];
|
|
5274
|
-
const settingsExist =
|
|
5520
|
+
const settingsExist = existsSync3(settingsPath);
|
|
5275
5521
|
if (!settingsExist) {
|
|
5276
|
-
issues.push({ hook: "(settings)", issue:
|
|
5522
|
+
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
5277
5523
|
}
|
|
5278
|
-
const
|
|
5279
|
-
const
|
|
5280
|
-
for (const name of installed) {
|
|
5524
|
+
const registered = getRegisteredHooks(scope);
|
|
5525
|
+
for (const name of registered) {
|
|
5281
5526
|
const meta = getHook(name);
|
|
5282
|
-
const hookDir = join2(process.cwd(), ".hooks", `hook-${name}`);
|
|
5283
5527
|
let hookHealthy = true;
|
|
5284
|
-
|
|
5285
|
-
|
|
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" });
|
|
5528
|
+
if (!hookExists(name)) {
|
|
5529
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
5291
5530
|
hookHealthy = false;
|
|
5531
|
+
continue;
|
|
5292
5532
|
}
|
|
5293
|
-
|
|
5294
|
-
|
|
5533
|
+
const hookDir = getHookPath(name);
|
|
5534
|
+
const hookScript = join3(hookDir, "src", "hook.ts");
|
|
5535
|
+
if (!existsSync3(hookScript)) {
|
|
5536
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
5295
5537
|
hookHealthy = false;
|
|
5296
5538
|
}
|
|
5297
5539
|
if (meta && settingsExist) {
|
|
5298
5540
|
try {
|
|
5299
|
-
const settings = JSON.parse(
|
|
5541
|
+
const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
5300
5542
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5301
|
-
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command
|
|
5302
|
-
if (
|
|
5303
|
-
issues.push({ hook: name, issue: `
|
|
5543
|
+
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
|
|
5544
|
+
if (!found) {
|
|
5545
|
+
issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
|
|
5304
5546
|
hookHealthy = false;
|
|
5305
5547
|
}
|
|
5306
5548
|
} catch {}
|
|
@@ -5309,20 +5551,15 @@ program2.command("doctor").option("-j, --json", "Output as JSON", false).descrip
|
|
|
5309
5551
|
healthy.push(name);
|
|
5310
5552
|
}
|
|
5311
5553
|
}
|
|
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
5554
|
if (options.json) {
|
|
5318
|
-
console.log(JSON.stringify({ healthy, issues,
|
|
5555
|
+
console.log(JSON.stringify({ healthy, issues, registered, scope }));
|
|
5319
5556
|
return;
|
|
5320
5557
|
}
|
|
5321
5558
|
console.log(chalk2.bold(`
|
|
5322
|
-
Hook Health Check
|
|
5559
|
+
Hook Health Check (${scope})
|
|
5323
5560
|
`));
|
|
5324
|
-
if (
|
|
5325
|
-
console.log(chalk2.dim(" No hooks
|
|
5561
|
+
if (registered.length === 0) {
|
|
5562
|
+
console.log(chalk2.dim(" No hooks registered."));
|
|
5326
5563
|
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
5327
5564
|
return;
|
|
5328
5565
|
}
|
|
@@ -5345,8 +5582,9 @@ Hook Health Check
|
|
|
5345
5582
|
}
|
|
5346
5583
|
console.log();
|
|
5347
5584
|
});
|
|
5348
|
-
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-j, --json", "Output as JSON", false).description("
|
|
5349
|
-
const
|
|
5585
|
+
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)").action((hooks, options) => {
|
|
5586
|
+
const scope = resolveScope(options);
|
|
5587
|
+
const installed = getInstalledHooks(scope);
|
|
5350
5588
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
5351
5589
|
if (toUpdate.length === 0) {
|
|
5352
5590
|
if (options.json) {
|
|
@@ -5362,7 +5600,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
5362
5600
|
results.push({ hook: name, success: false, error: "Not installed" });
|
|
5363
5601
|
continue;
|
|
5364
5602
|
}
|
|
5365
|
-
const result = installHook(name, { overwrite: true });
|
|
5603
|
+
const result = installHook(name, { scope, overwrite: true });
|
|
5366
5604
|
results.push(result);
|
|
5367
5605
|
}
|
|
5368
5606
|
if (options.json) {
|
|
@@ -5395,10 +5633,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
5395
5633
|
return;
|
|
5396
5634
|
}
|
|
5397
5635
|
const hookPath = getHookPath(hook);
|
|
5398
|
-
const readmePath =
|
|
5636
|
+
const readmePath = join3(hookPath, "README.md");
|
|
5399
5637
|
let readme = "";
|
|
5400
|
-
if (
|
|
5401
|
-
readme =
|
|
5638
|
+
if (existsSync3(readmePath)) {
|
|
5639
|
+
readme = readFileSync3(readmePath, "utf-8");
|
|
5402
5640
|
}
|
|
5403
5641
|
if (options.json) {
|
|
5404
5642
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -5412,24 +5650,24 @@ ${meta.displayName} v${meta.version}
|
|
|
5412
5650
|
console.log(chalk2.bold(" Configuration:"));
|
|
5413
5651
|
console.log(` Event: ${meta.event}`);
|
|
5414
5652
|
console.log(` Matcher: ${meta.matcher || "(all tools)"}`);
|
|
5415
|
-
console.log(`
|
|
5653
|
+
console.log(` Command: hooks run ${meta.name}`);
|
|
5416
5654
|
console.log();
|
|
5417
5655
|
console.log(chalk2.bold(" Install:"));
|
|
5418
|
-
console.log(` hooks install ${meta.name}`);
|
|
5656
|
+
console.log(` hooks install ${meta.name} # global`);
|
|
5657
|
+
console.log(` hooks install ${meta.name} --project # project only`);
|
|
5419
5658
|
console.log();
|
|
5420
5659
|
if (readme) {
|
|
5421
5660
|
console.log(chalk2.bold(` README:
|
|
5422
5661
|
`));
|
|
5423
|
-
const
|
|
5424
|
-
`)
|
|
5425
|
-
for (const line of lines) {
|
|
5662
|
+
for (const line of readme.split(`
|
|
5663
|
+
`)) {
|
|
5426
5664
|
console.log(` ${line}`);
|
|
5427
5665
|
}
|
|
5428
5666
|
}
|
|
5429
5667
|
return;
|
|
5430
5668
|
}
|
|
5431
5669
|
const generalDocs = {
|
|
5432
|
-
overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session.",
|
|
5670
|
+
overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session. Install @hasna/hooks globally, then register hooks \u2014 no files are copied to your project.",
|
|
5433
5671
|
events: {
|
|
5434
5672
|
PreToolUse: 'Fires before a tool executes. Can block the operation by returning { "decision": "block" }.',
|
|
5435
5673
|
PostToolUse: "Fires after a tool executes. Runs asynchronously, cannot block.",
|
|
@@ -5437,25 +5675,26 @@ ${meta.displayName} v${meta.version}
|
|
|
5437
5675
|
Notification: "Fires on notification events like context compaction."
|
|
5438
5676
|
},
|
|
5439
5677
|
installation: {
|
|
5440
|
-
|
|
5441
|
-
|
|
5678
|
+
global: "hooks install gitguard",
|
|
5679
|
+
project: "hooks install gitguard --project",
|
|
5442
5680
|
category: 'hooks install --category "Git Safety"',
|
|
5443
5681
|
all: "hooks install --all"
|
|
5444
5682
|
},
|
|
5445
5683
|
management: {
|
|
5446
5684
|
list: "hooks list",
|
|
5447
5685
|
listInstalled: "hooks list --installed",
|
|
5448
|
-
listRegistered: "hooks list --registered",
|
|
5449
5686
|
search: "hooks search <query>",
|
|
5450
5687
|
info: "hooks info <name>",
|
|
5451
5688
|
remove: "hooks remove <name>",
|
|
5452
5689
|
update: "hooks update",
|
|
5453
|
-
doctor: "hooks doctor"
|
|
5690
|
+
doctor: "hooks doctor",
|
|
5691
|
+
docs: "hooks docs <name>"
|
|
5454
5692
|
},
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5693
|
+
howItWorks: {
|
|
5694
|
+
install: "bun install -g @hasna/hooks",
|
|
5695
|
+
register: "hooks install gitguard \u2192 writes to ~/.claude/settings.json",
|
|
5696
|
+
execution: "Claude Code runs 'hooks run gitguard' \u2192 executes hook from global package",
|
|
5697
|
+
noFileCopy: "No files are copied to your project. Hooks run from the global @hasna/hooks package."
|
|
5459
5698
|
}
|
|
5460
5699
|
};
|
|
5461
5700
|
if (options.json) {
|
|
@@ -5469,7 +5708,13 @@ ${meta.displayName} v${meta.version}
|
|
|
5469
5708
|
`));
|
|
5470
5709
|
console.log(` ${generalDocs.overview}
|
|
5471
5710
|
`);
|
|
5472
|
-
console.log(chalk2.bold(`
|
|
5711
|
+
console.log(chalk2.bold(` How It Works
|
|
5712
|
+
`));
|
|
5713
|
+
for (const [label, desc] of Object.entries(generalDocs.howItWorks)) {
|
|
5714
|
+
console.log(` ${chalk2.dim(label + ":")} ${desc}`);
|
|
5715
|
+
}
|
|
5716
|
+
console.log(chalk2.bold(`
|
|
5717
|
+
Hook Events
|
|
5473
5718
|
`));
|
|
5474
5719
|
for (const [event, desc] of Object.entries(generalDocs.events)) {
|
|
5475
5720
|
console.log(` ${chalk2.cyan(event)}`);
|
|
@@ -5488,16 +5733,19 @@ ${meta.displayName} v${meta.version}
|
|
|
5488
5733
|
console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
|
|
5489
5734
|
}
|
|
5490
5735
|
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
5736
|
Hook-Specific Docs
|
|
5498
5737
|
`));
|
|
5499
5738
|
console.log(` hooks docs <name> View README for a specific hook`);
|
|
5500
5739
|
console.log(` hooks docs --json Machine-readable documentation`);
|
|
5501
5740
|
console.log();
|
|
5502
5741
|
});
|
|
5742
|
+
program2.command("mcp").option("-s, --stdio", "Use stdio transport (for Claude Code integration)", false).option("-p, --port <port>", "Port for SSE transport", "39427").description("Start MCP server for AI agent integration").action(async (options) => {
|
|
5743
|
+
if (options.stdio) {
|
|
5744
|
+
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
5745
|
+
await startStdioServer2();
|
|
5746
|
+
} else {
|
|
5747
|
+
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
5748
|
+
await startSSEServer2(parseInt(options.port));
|
|
5749
|
+
}
|
|
5750
|
+
});
|
|
5503
5751
|
program2.parse();
|