@hasna/hooks 0.0.6 → 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 +603 -319
- package/package.json +3 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "hooks run gitguard"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"matcher": "Bash"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"hooks": [
|
|
15
|
+
{
|
|
16
|
+
"type": "command",
|
|
17
|
+
"command": "hooks run packageage"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"matcher": "Bash"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
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,8 +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
|
|
4097
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4098
|
+
import { join as join3 } from "path";
|
|
3524
4099
|
|
|
3525
4100
|
// src/cli/components/App.tsx
|
|
3526
4101
|
import { useState as useState7 } from "react";
|
|
@@ -3952,179 +4527,7 @@ function Header({ title = "Hooks", subtitle }) {
|
|
|
3952
4527
|
|
|
3953
4528
|
// src/cli/components/CategorySelect.tsx
|
|
3954
4529
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
3955
|
-
|
|
3956
|
-
// src/lib/registry.ts
|
|
3957
|
-
var CATEGORIES = [
|
|
3958
|
-
"Git Safety",
|
|
3959
|
-
"Code Quality",
|
|
3960
|
-
"Security",
|
|
3961
|
-
"Notifications",
|
|
3962
|
-
"Context Management"
|
|
3963
|
-
];
|
|
3964
|
-
var HOOKS = [
|
|
3965
|
-
{
|
|
3966
|
-
name: "gitguard",
|
|
3967
|
-
displayName: "Git Guard",
|
|
3968
|
-
description: "Blocks destructive git operations like reset --hard, push --force, clean -f",
|
|
3969
|
-
version: "0.1.0",
|
|
3970
|
-
category: "Git Safety",
|
|
3971
|
-
event: "PreToolUse",
|
|
3972
|
-
matcher: "Bash",
|
|
3973
|
-
tags: ["git", "safety", "destructive", "guard"]
|
|
3974
|
-
},
|
|
3975
|
-
{
|
|
3976
|
-
name: "branchprotect",
|
|
3977
|
-
displayName: "Branch Protect",
|
|
3978
|
-
description: "Prevents editing files directly on main/master branch",
|
|
3979
|
-
version: "0.1.0",
|
|
3980
|
-
category: "Git Safety",
|
|
3981
|
-
event: "PreToolUse",
|
|
3982
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
3983
|
-
tags: ["git", "branch", "protection", "main"]
|
|
3984
|
-
},
|
|
3985
|
-
{
|
|
3986
|
-
name: "checkpoint",
|
|
3987
|
-
displayName: "Checkpoint",
|
|
3988
|
-
description: "Creates shadow git snapshots before file modifications for easy rollback",
|
|
3989
|
-
version: "0.1.0",
|
|
3990
|
-
category: "Git Safety",
|
|
3991
|
-
event: "PreToolUse",
|
|
3992
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
3993
|
-
tags: ["git", "snapshot", "rollback", "backup"]
|
|
3994
|
-
},
|
|
3995
|
-
{
|
|
3996
|
-
name: "checktests",
|
|
3997
|
-
displayName: "Check Tests",
|
|
3998
|
-
description: "Checks for missing tests after file edits",
|
|
3999
|
-
version: "0.1.6",
|
|
4000
|
-
category: "Code Quality",
|
|
4001
|
-
event: "PostToolUse",
|
|
4002
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4003
|
-
tags: ["tests", "coverage", "quality"]
|
|
4004
|
-
},
|
|
4005
|
-
{
|
|
4006
|
-
name: "checklint",
|
|
4007
|
-
displayName: "Check Lint",
|
|
4008
|
-
description: "Runs linting after file edits and creates tasks for errors",
|
|
4009
|
-
version: "0.1.7",
|
|
4010
|
-
category: "Code Quality",
|
|
4011
|
-
event: "PostToolUse",
|
|
4012
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4013
|
-
tags: ["lint", "style", "quality"]
|
|
4014
|
-
},
|
|
4015
|
-
{
|
|
4016
|
-
name: "checkfiles",
|
|
4017
|
-
displayName: "Check Files",
|
|
4018
|
-
description: "Runs headless agent to review files and create tasks",
|
|
4019
|
-
version: "0.1.4",
|
|
4020
|
-
category: "Code Quality",
|
|
4021
|
-
event: "PostToolUse",
|
|
4022
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4023
|
-
tags: ["review", "files", "quality"]
|
|
4024
|
-
},
|
|
4025
|
-
{
|
|
4026
|
-
name: "checkbugs",
|
|
4027
|
-
displayName: "Check Bugs",
|
|
4028
|
-
description: "Checks for bugs via Codex headless agent",
|
|
4029
|
-
version: "0.1.6",
|
|
4030
|
-
category: "Code Quality",
|
|
4031
|
-
event: "PostToolUse",
|
|
4032
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4033
|
-
tags: ["bugs", "analysis", "quality"]
|
|
4034
|
-
},
|
|
4035
|
-
{
|
|
4036
|
-
name: "checkdocs",
|
|
4037
|
-
displayName: "Check Docs",
|
|
4038
|
-
description: "Checks for missing documentation and creates tasks",
|
|
4039
|
-
version: "0.2.1",
|
|
4040
|
-
category: "Code Quality",
|
|
4041
|
-
event: "PostToolUse",
|
|
4042
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4043
|
-
tags: ["docs", "documentation", "quality"]
|
|
4044
|
-
},
|
|
4045
|
-
{
|
|
4046
|
-
name: "checktasks",
|
|
4047
|
-
displayName: "Check Tasks",
|
|
4048
|
-
description: "Validates task completion and tracks progress",
|
|
4049
|
-
version: "1.0.8",
|
|
4050
|
-
category: "Code Quality",
|
|
4051
|
-
event: "PostToolUse",
|
|
4052
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4053
|
-
tags: ["tasks", "tracking", "quality"]
|
|
4054
|
-
},
|
|
4055
|
-
{
|
|
4056
|
-
name: "checksecurity",
|
|
4057
|
-
displayName: "Check Security",
|
|
4058
|
-
description: "Runs security checks via Claude and Codex headless agents",
|
|
4059
|
-
version: "0.1.6",
|
|
4060
|
-
category: "Security",
|
|
4061
|
-
event: "PostToolUse",
|
|
4062
|
-
matcher: "Edit|Write|NotebookEdit",
|
|
4063
|
-
tags: ["security", "audit", "vulnerabilities"]
|
|
4064
|
-
},
|
|
4065
|
-
{
|
|
4066
|
-
name: "packageage",
|
|
4067
|
-
displayName: "Package Age",
|
|
4068
|
-
description: "Checks package age before install to prevent typosquatting",
|
|
4069
|
-
version: "0.1.1",
|
|
4070
|
-
category: "Security",
|
|
4071
|
-
event: "PreToolUse",
|
|
4072
|
-
matcher: "Bash",
|
|
4073
|
-
tags: ["npm", "packages", "typosquatting", "supply-chain"]
|
|
4074
|
-
},
|
|
4075
|
-
{
|
|
4076
|
-
name: "phonenotify",
|
|
4077
|
-
displayName: "Phone Notify",
|
|
4078
|
-
description: "Sends push notifications to phone via ntfy.sh",
|
|
4079
|
-
version: "0.1.0",
|
|
4080
|
-
category: "Notifications",
|
|
4081
|
-
event: "Stop",
|
|
4082
|
-
matcher: "",
|
|
4083
|
-
tags: ["notification", "phone", "push", "ntfy"]
|
|
4084
|
-
},
|
|
4085
|
-
{
|
|
4086
|
-
name: "agentmessages",
|
|
4087
|
-
displayName: "Agent Messages",
|
|
4088
|
-
description: "Inter-agent messaging integration for service-message",
|
|
4089
|
-
version: "0.1.0",
|
|
4090
|
-
category: "Notifications",
|
|
4091
|
-
event: "Stop",
|
|
4092
|
-
matcher: "",
|
|
4093
|
-
tags: ["messaging", "agents", "inter-agent"]
|
|
4094
|
-
},
|
|
4095
|
-
{
|
|
4096
|
-
name: "contextrefresh",
|
|
4097
|
-
displayName: "Context Refresh",
|
|
4098
|
-
description: "Re-injects important context every N prompts to prevent drift",
|
|
4099
|
-
version: "0.1.0",
|
|
4100
|
-
category: "Context Management",
|
|
4101
|
-
event: "Notification",
|
|
4102
|
-
matcher: "",
|
|
4103
|
-
tags: ["context", "memory", "prompts", "refresh"]
|
|
4104
|
-
},
|
|
4105
|
-
{
|
|
4106
|
-
name: "precompact",
|
|
4107
|
-
displayName: "Pre-Compact",
|
|
4108
|
-
description: "Saves session state before context compaction",
|
|
4109
|
-
version: "0.1.0",
|
|
4110
|
-
category: "Context Management",
|
|
4111
|
-
event: "Notification",
|
|
4112
|
-
matcher: "",
|
|
4113
|
-
tags: ["context", "compaction", "state", "backup"]
|
|
4114
|
-
}
|
|
4115
|
-
];
|
|
4116
|
-
function getHooksByCategory(category) {
|
|
4117
|
-
return HOOKS.filter((h) => h.category === category);
|
|
4118
|
-
}
|
|
4119
|
-
function searchHooks(query) {
|
|
4120
|
-
const q = query.toLowerCase();
|
|
4121
|
-
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)));
|
|
4122
|
-
}
|
|
4123
|
-
function getHook(name) {
|
|
4124
|
-
return HOOKS.find((h) => h.name === name);
|
|
4125
|
-
}
|
|
4126
|
-
|
|
4127
|
-
// src/cli/components/CategorySelect.tsx
|
|
4530
|
+
init_registry();
|
|
4128
4531
|
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
4129
4532
|
function CategorySelect({ onSelect, onBack }) {
|
|
4130
4533
|
const items = CATEGORIES.map((cat) => ({
|
|
@@ -4414,6 +4817,7 @@ function TextInput({ value: originalValue, placeholder = "", focus = true, mask,
|
|
|
4414
4817
|
var build_default = TextInput;
|
|
4415
4818
|
|
|
4416
4819
|
// src/cli/components/SearchView.tsx
|
|
4820
|
+
init_registry();
|
|
4417
4821
|
import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
|
|
4418
4822
|
function SearchView({
|
|
4419
4823
|
selected,
|
|
@@ -4520,141 +4924,9 @@ function Spinner({ type = "dots" }) {
|
|
|
4520
4924
|
}
|
|
4521
4925
|
var build_default2 = Spinner;
|
|
4522
4926
|
|
|
4523
|
-
// src/lib/installer.ts
|
|
4524
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4525
|
-
import { join, dirname } from "path";
|
|
4526
|
-
import { homedir } from "os";
|
|
4527
|
-
import { fileURLToPath } from "url";
|
|
4528
|
-
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
4529
|
-
var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
|
|
4530
|
-
function getSettingsPath(scope = "global") {
|
|
4531
|
-
if (scope === "project") {
|
|
4532
|
-
return join(process.cwd(), ".claude", "settings.json");
|
|
4533
|
-
}
|
|
4534
|
-
return join(homedir(), ".claude", "settings.json");
|
|
4535
|
-
}
|
|
4536
|
-
function getHookPath(name) {
|
|
4537
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4538
|
-
return join(HOOKS_DIR, hookName);
|
|
4539
|
-
}
|
|
4540
|
-
function hookExists(name) {
|
|
4541
|
-
return existsSync(getHookPath(name));
|
|
4542
|
-
}
|
|
4543
|
-
function readSettings(scope = "global") {
|
|
4544
|
-
const path = getSettingsPath(scope);
|
|
4545
|
-
try {
|
|
4546
|
-
if (existsSync(path)) {
|
|
4547
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4548
|
-
}
|
|
4549
|
-
} catch {}
|
|
4550
|
-
return {};
|
|
4551
|
-
}
|
|
4552
|
-
function writeSettings(settings, scope = "global") {
|
|
4553
|
-
const path = getSettingsPath(scope);
|
|
4554
|
-
const dir = dirname(path);
|
|
4555
|
-
if (!existsSync(dir)) {
|
|
4556
|
-
mkdirSync(dir, { recursive: true });
|
|
4557
|
-
}
|
|
4558
|
-
writeFileSync(path, JSON.stringify(settings, null, 2) + `
|
|
4559
|
-
`);
|
|
4560
|
-
}
|
|
4561
|
-
function installHook(name, options = {}) {
|
|
4562
|
-
const { scope = "global", overwrite = false } = options;
|
|
4563
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4564
|
-
const shortName = hookName.replace("hook-", "");
|
|
4565
|
-
if (!hookExists(shortName)) {
|
|
4566
|
-
return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
|
|
4567
|
-
}
|
|
4568
|
-
const registered = getRegisteredHooks(scope);
|
|
4569
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
4570
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
|
|
4571
|
-
}
|
|
4572
|
-
try {
|
|
4573
|
-
registerHook(shortName, scope);
|
|
4574
|
-
return { hook: shortName, success: true, scope };
|
|
4575
|
-
} catch (error) {
|
|
4576
|
-
return {
|
|
4577
|
-
hook: shortName,
|
|
4578
|
-
success: false,
|
|
4579
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
4580
|
-
};
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
function registerHook(name, scope = "global") {
|
|
4584
|
-
const meta = getHook(name);
|
|
4585
|
-
if (!meta)
|
|
4586
|
-
return;
|
|
4587
|
-
const settings = readSettings(scope);
|
|
4588
|
-
if (!settings.hooks)
|
|
4589
|
-
settings.hooks = {};
|
|
4590
|
-
const eventKey = meta.event;
|
|
4591
|
-
if (!settings.hooks[eventKey])
|
|
4592
|
-
settings.hooks[eventKey] = [];
|
|
4593
|
-
const hookCommand = `hooks run ${name}`;
|
|
4594
|
-
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
|
|
4595
|
-
const entry = {
|
|
4596
|
-
hooks: [{ type: "command", command: hookCommand }]
|
|
4597
|
-
};
|
|
4598
|
-
if (meta.matcher) {
|
|
4599
|
-
entry.matcher = meta.matcher;
|
|
4600
|
-
}
|
|
4601
|
-
settings.hooks[eventKey].push(entry);
|
|
4602
|
-
writeSettings(settings, scope);
|
|
4603
|
-
}
|
|
4604
|
-
function unregisterHook(name, scope = "global") {
|
|
4605
|
-
const meta = getHook(name);
|
|
4606
|
-
if (!meta)
|
|
4607
|
-
return;
|
|
4608
|
-
const settings = readSettings(scope);
|
|
4609
|
-
if (!settings.hooks)
|
|
4610
|
-
return;
|
|
4611
|
-
const eventKey = meta.event;
|
|
4612
|
-
if (!settings.hooks[eventKey])
|
|
4613
|
-
return;
|
|
4614
|
-
const hookCommand = `hooks run ${name}`;
|
|
4615
|
-
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
|
|
4616
|
-
if (settings.hooks[eventKey].length === 0) {
|
|
4617
|
-
delete settings.hooks[eventKey];
|
|
4618
|
-
}
|
|
4619
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
4620
|
-
delete settings.hooks;
|
|
4621
|
-
}
|
|
4622
|
-
writeSettings(settings, scope);
|
|
4623
|
-
}
|
|
4624
|
-
function getRegisteredHooks(scope = "global") {
|
|
4625
|
-
const settings = readSettings(scope);
|
|
4626
|
-
if (!settings.hooks)
|
|
4627
|
-
return [];
|
|
4628
|
-
const registered = [];
|
|
4629
|
-
for (const eventKey of Object.keys(settings.hooks)) {
|
|
4630
|
-
for (const entry of settings.hooks[eventKey]) {
|
|
4631
|
-
for (const hook of entry.hooks || []) {
|
|
4632
|
-
const newMatch = hook.command?.match(/^hooks run (\w+)$/);
|
|
4633
|
-
const oldMatch = hook.command?.match(/^hook-(\w+)$/);
|
|
4634
|
-
const match = newMatch || oldMatch;
|
|
4635
|
-
if (match) {
|
|
4636
|
-
registered.push(match[1]);
|
|
4637
|
-
}
|
|
4638
|
-
}
|
|
4639
|
-
}
|
|
4640
|
-
}
|
|
4641
|
-
return [...new Set(registered)];
|
|
4642
|
-
}
|
|
4643
|
-
function getInstalledHooks(scope = "global") {
|
|
4644
|
-
return getRegisteredHooks(scope);
|
|
4645
|
-
}
|
|
4646
|
-
function removeHook(name, scope = "global") {
|
|
4647
|
-
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4648
|
-
const shortName = hookName.replace("hook-", "");
|
|
4649
|
-
const registered = getRegisteredHooks(scope);
|
|
4650
|
-
if (!registered.includes(shortName)) {
|
|
4651
|
-
return false;
|
|
4652
|
-
}
|
|
4653
|
-
unregisterHook(shortName, scope);
|
|
4654
|
-
return true;
|
|
4655
|
-
}
|
|
4656
|
-
|
|
4657
4927
|
// src/cli/components/InstallProgress.tsx
|
|
4928
|
+
init_installer();
|
|
4929
|
+
init_registry();
|
|
4658
4930
|
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
4659
4931
|
function InstallProgress({
|
|
4660
4932
|
hooks,
|
|
@@ -4782,6 +5054,7 @@ function InstallProgress({
|
|
|
4782
5054
|
}
|
|
4783
5055
|
|
|
4784
5056
|
// src/cli/components/App.tsx
|
|
5057
|
+
init_registry();
|
|
4785
5058
|
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
4786
5059
|
function App({ initialHooks, overwrite = false }) {
|
|
4787
5060
|
const { exit } = useApp();
|
|
@@ -4988,6 +5261,8 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
4988
5261
|
}
|
|
4989
5262
|
|
|
4990
5263
|
// src/cli/index.tsx
|
|
5264
|
+
init_registry();
|
|
5265
|
+
init_installer();
|
|
4991
5266
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
4992
5267
|
var program2 = new Command;
|
|
4993
5268
|
function resolveScope(options) {
|
|
@@ -4995,7 +5270,7 @@ function resolveScope(options) {
|
|
|
4995
5270
|
return "project";
|
|
4996
5271
|
return "global";
|
|
4997
5272
|
}
|
|
4998
|
-
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.
|
|
5273
|
+
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.7");
|
|
4999
5274
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
5000
5275
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5001
5276
|
});
|
|
@@ -5006,8 +5281,8 @@ program2.command("run").argument("<hook>", "Hook to run").description("Execute a
|
|
|
5006
5281
|
process.exit(1);
|
|
5007
5282
|
}
|
|
5008
5283
|
const hookDir = getHookPath(hook);
|
|
5009
|
-
const hookScript =
|
|
5010
|
-
if (!
|
|
5284
|
+
const hookScript = join3(hookDir, "src", "hook.ts");
|
|
5285
|
+
if (!existsSync3(hookScript)) {
|
|
5011
5286
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5012
5287
|
process.exit(1);
|
|
5013
5288
|
}
|
|
@@ -5242,7 +5517,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
5242
5517
|
const settingsPath = getSettingsPath(scope);
|
|
5243
5518
|
const issues = [];
|
|
5244
5519
|
const healthy = [];
|
|
5245
|
-
const settingsExist =
|
|
5520
|
+
const settingsExist = existsSync3(settingsPath);
|
|
5246
5521
|
if (!settingsExist) {
|
|
5247
5522
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
5248
5523
|
}
|
|
@@ -5256,14 +5531,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
5256
5531
|
continue;
|
|
5257
5532
|
}
|
|
5258
5533
|
const hookDir = getHookPath(name);
|
|
5259
|
-
const hookScript =
|
|
5260
|
-
if (!
|
|
5534
|
+
const hookScript = join3(hookDir, "src", "hook.ts");
|
|
5535
|
+
if (!existsSync3(hookScript)) {
|
|
5261
5536
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
5262
5537
|
hookHealthy = false;
|
|
5263
5538
|
}
|
|
5264
5539
|
if (meta && settingsExist) {
|
|
5265
5540
|
try {
|
|
5266
|
-
const settings = JSON.parse(
|
|
5541
|
+
const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
5267
5542
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5268
5543
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
|
|
5269
5544
|
if (!found) {
|
|
@@ -5358,10 +5633,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
5358
5633
|
return;
|
|
5359
5634
|
}
|
|
5360
5635
|
const hookPath = getHookPath(hook);
|
|
5361
|
-
const readmePath =
|
|
5636
|
+
const readmePath = join3(hookPath, "README.md");
|
|
5362
5637
|
let readme = "";
|
|
5363
|
-
if (
|
|
5364
|
-
readme =
|
|
5638
|
+
if (existsSync3(readmePath)) {
|
|
5639
|
+
readme = readFileSync3(readmePath, "utf-8");
|
|
5365
5640
|
}
|
|
5366
5641
|
if (options.json) {
|
|
5367
5642
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -5464,4 +5739,13 @@ ${meta.displayName} v${meta.version}
|
|
|
5464
5739
|
console.log(` hooks docs --json Machine-readable documentation`);
|
|
5465
5740
|
console.log();
|
|
5466
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
|
+
});
|
|
5467
5751
|
program2.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Open source Claude Code hooks library - Install hooks with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/index.ts --outdir ./dist --target bun",
|
|
18
|
+
"build": "bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/index.ts --outdir ./dist --target bun",
|
|
19
19
|
"dev": "bun run ./src/cli/index.tsx",
|
|
20
20
|
"test": "bun test",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"typescript": "^5"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
42
43
|
"chalk": "^5.3.0",
|
|
43
44
|
"commander": "^12.1.0",
|
|
44
45
|
"conf": "^13.0.1",
|