@hasna/hooks 0.0.1 ā 0.0.2
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/dist/index.js +366 -0
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/package.json +2 -2
- package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
- package/hooks/hook-agentmessages/src/install.ts +0 -126
- package/hooks/hook-agentmessages/src/session-start.ts +0 -255
- package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
- package/hooks/hook-branchprotect/src/cli.ts +0 -126
- package/hooks/hook-branchprotect/src/hook.ts +0 -88
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/src/cli.ts +0 -628
- package/hooks/hook-checkbugs/src/hook.ts +0 -335
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/src/cli.ts +0 -628
- package/hooks/hook-checkdocs/src/hook.ts +0 -310
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/src/cli.ts +0 -545
- package/hooks/hook-checkfiles/src/hook.ts +0 -321
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/src/cli-patch.ts +0 -32
- package/hooks/hook-checklint/src/cli.ts +0 -667
- package/hooks/hook-checklint/src/hook.ts +0 -473
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/src/cli.ts +0 -191
- package/hooks/hook-checkpoint/src/hook.ts +0 -207
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/src/cli.ts +0 -601
- package/hooks/hook-checksecurity/src/hook.ts +0 -334
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/src/cli.ts +0 -578
- package/hooks/hook-checktasks/src/hook.ts +0 -308
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/src/cli.ts +0 -627
- package/hooks/hook-checktests/src/hook.ts +0 -334
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-contextrefresh/src/cli.ts +0 -152
- package/hooks/hook-contextrefresh/src/hook.ts +0 -148
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-gitguard/src/cli.ts +0 -159
- package/hooks/hook-gitguard/src/hook.ts +0 -129
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-packageage/src/cli.ts +0 -165
- package/hooks/hook-packageage/src/hook.ts +0 -177
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/src/cli.ts +0 -196
- package/hooks/hook-phonenotify/src/hook.ts +0 -139
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/src/cli.ts +0 -168
- package/hooks/hook-precompact/src/hook.ts +0 -122
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/src/cli/components/App.tsx +0 -191
- package/src/cli/components/CategorySelect.tsx +0 -37
- package/src/cli/components/DataTable.tsx +0 -133
- package/src/cli/components/Header.tsx +0 -18
- package/src/cli/components/HookSelect.tsx +0 -29
- package/src/cli/components/InstallProgress.tsx +0 -105
- package/src/cli/components/SearchView.tsx +0 -86
- package/src/cli/index.tsx +0 -218
- package/src/index.ts +0 -31
- package/src/lib/installer.ts +0 -288
- package/src/lib/registry.ts +0 -205
- package/tsconfig.json +0 -17
package/dist/index.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/lib/registry.ts
|
|
5
|
+
var CATEGORIES = [
|
|
6
|
+
"Git Safety",
|
|
7
|
+
"Code Quality",
|
|
8
|
+
"Security",
|
|
9
|
+
"Notifications",
|
|
10
|
+
"Context Management"
|
|
11
|
+
];
|
|
12
|
+
var HOOKS = [
|
|
13
|
+
{
|
|
14
|
+
name: "gitguard",
|
|
15
|
+
displayName: "Git Guard",
|
|
16
|
+
description: "Blocks destructive git operations like reset --hard, push --force, clean -f",
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
category: "Git Safety",
|
|
19
|
+
event: "PreToolUse",
|
|
20
|
+
matcher: "Bash",
|
|
21
|
+
tags: ["git", "safety", "destructive", "guard"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "branchprotect",
|
|
25
|
+
displayName: "Branch Protect",
|
|
26
|
+
description: "Prevents editing files directly on main/master branch",
|
|
27
|
+
version: "0.1.0",
|
|
28
|
+
category: "Git Safety",
|
|
29
|
+
event: "PreToolUse",
|
|
30
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
31
|
+
tags: ["git", "branch", "protection", "main"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "checkpoint",
|
|
35
|
+
displayName: "Checkpoint",
|
|
36
|
+
description: "Creates shadow git snapshots before file modifications for easy rollback",
|
|
37
|
+
version: "0.1.0",
|
|
38
|
+
category: "Git Safety",
|
|
39
|
+
event: "PreToolUse",
|
|
40
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
41
|
+
tags: ["git", "snapshot", "rollback", "backup"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "checktests",
|
|
45
|
+
displayName: "Check Tests",
|
|
46
|
+
description: "Checks for missing tests after file edits",
|
|
47
|
+
version: "0.1.6",
|
|
48
|
+
category: "Code Quality",
|
|
49
|
+
event: "PostToolUse",
|
|
50
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
51
|
+
tags: ["tests", "coverage", "quality"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "checklint",
|
|
55
|
+
displayName: "Check Lint",
|
|
56
|
+
description: "Runs linting after file edits and creates tasks for errors",
|
|
57
|
+
version: "0.1.7",
|
|
58
|
+
category: "Code Quality",
|
|
59
|
+
event: "PostToolUse",
|
|
60
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
61
|
+
tags: ["lint", "style", "quality"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "checkfiles",
|
|
65
|
+
displayName: "Check Files",
|
|
66
|
+
description: "Runs headless agent to review files and create tasks",
|
|
67
|
+
version: "0.1.4",
|
|
68
|
+
category: "Code Quality",
|
|
69
|
+
event: "PostToolUse",
|
|
70
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
71
|
+
tags: ["review", "files", "quality"]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "checkbugs",
|
|
75
|
+
displayName: "Check Bugs",
|
|
76
|
+
description: "Checks for bugs via Codex headless agent",
|
|
77
|
+
version: "0.1.6",
|
|
78
|
+
category: "Code Quality",
|
|
79
|
+
event: "PostToolUse",
|
|
80
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
81
|
+
tags: ["bugs", "analysis", "quality"]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "checkdocs",
|
|
85
|
+
displayName: "Check Docs",
|
|
86
|
+
description: "Checks for missing documentation and creates tasks",
|
|
87
|
+
version: "0.2.1",
|
|
88
|
+
category: "Code Quality",
|
|
89
|
+
event: "PostToolUse",
|
|
90
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
91
|
+
tags: ["docs", "documentation", "quality"]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "checktasks",
|
|
95
|
+
displayName: "Check Tasks",
|
|
96
|
+
description: "Validates task completion and tracks progress",
|
|
97
|
+
version: "1.0.8",
|
|
98
|
+
category: "Code Quality",
|
|
99
|
+
event: "PostToolUse",
|
|
100
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
101
|
+
tags: ["tasks", "tracking", "quality"]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "checksecurity",
|
|
105
|
+
displayName: "Check Security",
|
|
106
|
+
description: "Runs security checks via Claude and Codex headless agents",
|
|
107
|
+
version: "0.1.6",
|
|
108
|
+
category: "Security",
|
|
109
|
+
event: "PostToolUse",
|
|
110
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
111
|
+
tags: ["security", "audit", "vulnerabilities"]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "packageage",
|
|
115
|
+
displayName: "Package Age",
|
|
116
|
+
description: "Checks package age before install to prevent typosquatting",
|
|
117
|
+
version: "0.1.1",
|
|
118
|
+
category: "Security",
|
|
119
|
+
event: "PreToolUse",
|
|
120
|
+
matcher: "Bash",
|
|
121
|
+
tags: ["npm", "packages", "typosquatting", "supply-chain"]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "phonenotify",
|
|
125
|
+
displayName: "Phone Notify",
|
|
126
|
+
description: "Sends push notifications to phone via ntfy.sh",
|
|
127
|
+
version: "0.1.0",
|
|
128
|
+
category: "Notifications",
|
|
129
|
+
event: "Stop",
|
|
130
|
+
matcher: "",
|
|
131
|
+
tags: ["notification", "phone", "push", "ntfy"]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "agentmessages",
|
|
135
|
+
displayName: "Agent Messages",
|
|
136
|
+
description: "Inter-agent messaging integration for service-message",
|
|
137
|
+
version: "0.1.0",
|
|
138
|
+
category: "Notifications",
|
|
139
|
+
event: "Stop",
|
|
140
|
+
matcher: "",
|
|
141
|
+
tags: ["messaging", "agents", "inter-agent"]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "contextrefresh",
|
|
145
|
+
displayName: "Context Refresh",
|
|
146
|
+
description: "Re-injects important context every N prompts to prevent drift",
|
|
147
|
+
version: "0.1.0",
|
|
148
|
+
category: "Context Management",
|
|
149
|
+
event: "Notification",
|
|
150
|
+
matcher: "",
|
|
151
|
+
tags: ["context", "memory", "prompts", "refresh"]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "precompact",
|
|
155
|
+
displayName: "Pre-Compact",
|
|
156
|
+
description: "Saves session state before context compaction",
|
|
157
|
+
version: "0.1.0",
|
|
158
|
+
category: "Context Management",
|
|
159
|
+
event: "Notification",
|
|
160
|
+
matcher: "",
|
|
161
|
+
tags: ["context", "compaction", "state", "backup"]
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
function getHooksByCategory(category) {
|
|
165
|
+
return HOOKS.filter((h) => h.category === category);
|
|
166
|
+
}
|
|
167
|
+
function searchHooks(query) {
|
|
168
|
+
const q = query.toLowerCase();
|
|
169
|
+
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)));
|
|
170
|
+
}
|
|
171
|
+
function getHook(name) {
|
|
172
|
+
return HOOKS.find((h) => h.name === name);
|
|
173
|
+
}
|
|
174
|
+
// src/lib/installer.ts
|
|
175
|
+
import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
176
|
+
import { join, dirname } from "path";
|
|
177
|
+
import { homedir } from "os";
|
|
178
|
+
import { fileURLToPath } from "url";
|
|
179
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
180
|
+
var HOOKS_DIR = join(__dirname2, "..", "..", "hooks");
|
|
181
|
+
var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
182
|
+
function getHookPath(name) {
|
|
183
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
184
|
+
return join(HOOKS_DIR, hookName);
|
|
185
|
+
}
|
|
186
|
+
function hookExists(name) {
|
|
187
|
+
return existsSync(getHookPath(name));
|
|
188
|
+
}
|
|
189
|
+
function readSettings() {
|
|
190
|
+
try {
|
|
191
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
192
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
function writeSettings(settings) {
|
|
198
|
+
const dir = dirname(SETTINGS_PATH);
|
|
199
|
+
if (!existsSync(dir)) {
|
|
200
|
+
mkdirSync(dir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
function installHook(name, options = {}) {
|
|
206
|
+
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
207
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
208
|
+
const shortName = hookName.replace("hook-", "");
|
|
209
|
+
const sourcePath = getHookPath(name);
|
|
210
|
+
const destDir = join(targetDir, ".hooks");
|
|
211
|
+
const destPath = join(destDir, hookName);
|
|
212
|
+
if (!existsSync(sourcePath)) {
|
|
213
|
+
return {
|
|
214
|
+
hook: shortName,
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Hook '${shortName}' not found`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (existsSync(destPath) && !overwrite) {
|
|
220
|
+
return {
|
|
221
|
+
hook: shortName,
|
|
222
|
+
success: false,
|
|
223
|
+
error: `Already installed. Use --overwrite to replace.`,
|
|
224
|
+
path: destPath
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
if (!existsSync(destDir)) {
|
|
229
|
+
mkdirSync(destDir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
cpSync(sourcePath, destPath, { recursive: true });
|
|
232
|
+
registerHookInSettings(shortName);
|
|
233
|
+
updateHooksIndex(destDir);
|
|
234
|
+
return {
|
|
235
|
+
hook: shortName,
|
|
236
|
+
success: true,
|
|
237
|
+
path: destPath
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
hook: shortName,
|
|
242
|
+
success: false,
|
|
243
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function registerHookInSettings(name) {
|
|
248
|
+
const meta = getHook(name);
|
|
249
|
+
if (!meta)
|
|
250
|
+
return;
|
|
251
|
+
const settings = readSettings();
|
|
252
|
+
if (!settings.hooks)
|
|
253
|
+
settings.hooks = {};
|
|
254
|
+
const eventKey = meta.event;
|
|
255
|
+
if (!settings.hooks[eventKey])
|
|
256
|
+
settings.hooks[eventKey] = [];
|
|
257
|
+
const hookCommand = `hook-${name}`;
|
|
258
|
+
const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
|
|
259
|
+
if (existing)
|
|
260
|
+
return;
|
|
261
|
+
const entry = {
|
|
262
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
263
|
+
};
|
|
264
|
+
if (meta.matcher) {
|
|
265
|
+
entry.matcher = meta.matcher;
|
|
266
|
+
}
|
|
267
|
+
settings.hooks[eventKey].push(entry);
|
|
268
|
+
writeSettings(settings);
|
|
269
|
+
}
|
|
270
|
+
function unregisterHookFromSettings(name) {
|
|
271
|
+
const meta = getHook(name);
|
|
272
|
+
if (!meta)
|
|
273
|
+
return;
|
|
274
|
+
const settings = readSettings();
|
|
275
|
+
if (!settings.hooks)
|
|
276
|
+
return;
|
|
277
|
+
const eventKey = meta.event;
|
|
278
|
+
if (!settings.hooks[eventKey])
|
|
279
|
+
return;
|
|
280
|
+
const hookCommand = `hook-${name}`;
|
|
281
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
|
|
282
|
+
if (settings.hooks[eventKey].length === 0) {
|
|
283
|
+
delete settings.hooks[eventKey];
|
|
284
|
+
}
|
|
285
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
286
|
+
delete settings.hooks;
|
|
287
|
+
}
|
|
288
|
+
writeSettings(settings);
|
|
289
|
+
}
|
|
290
|
+
function installHooks(names, options = {}) {
|
|
291
|
+
return names.map((name) => installHook(name, options));
|
|
292
|
+
}
|
|
293
|
+
function updateHooksIndex(hooksDir) {
|
|
294
|
+
const indexPath = join(hooksDir, "index.ts");
|
|
295
|
+
const { readdirSync } = __require("fs");
|
|
296
|
+
const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
|
|
297
|
+
const exports = hooks.map((h) => {
|
|
298
|
+
const name = h.replace("hook-", "");
|
|
299
|
+
return `export * as ${name} from './${h}/src/index.js';`;
|
|
300
|
+
}).join(`
|
|
301
|
+
`);
|
|
302
|
+
const content = `/**
|
|
303
|
+
* Auto-generated index of installed hooks
|
|
304
|
+
* Do not edit manually - run 'hooks install' to update
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
${exports}
|
|
308
|
+
`;
|
|
309
|
+
writeFileSync(indexPath, content);
|
|
310
|
+
}
|
|
311
|
+
function getInstalledHooks(targetDir = process.cwd()) {
|
|
312
|
+
const hooksDir = join(targetDir, ".hooks");
|
|
313
|
+
if (!existsSync(hooksDir)) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const { readdirSync, statSync } = __require("fs");
|
|
317
|
+
return readdirSync(hooksDir).filter((f) => {
|
|
318
|
+
const fullPath = join(hooksDir, f);
|
|
319
|
+
return f.startsWith("hook-") && statSync(fullPath).isDirectory();
|
|
320
|
+
}).map((f) => f.replace("hook-", ""));
|
|
321
|
+
}
|
|
322
|
+
function getRegisteredHooks() {
|
|
323
|
+
const settings = readSettings();
|
|
324
|
+
if (!settings.hooks)
|
|
325
|
+
return [];
|
|
326
|
+
const registered = [];
|
|
327
|
+
for (const eventKey of Object.keys(settings.hooks)) {
|
|
328
|
+
for (const entry of settings.hooks[eventKey]) {
|
|
329
|
+
for (const hook of entry.hooks || []) {
|
|
330
|
+
const match = hook.command?.match(/hook-(\w+)/);
|
|
331
|
+
if (match) {
|
|
332
|
+
registered.push(match[1]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return [...new Set(registered)];
|
|
338
|
+
}
|
|
339
|
+
function removeHook(name, targetDir = process.cwd()) {
|
|
340
|
+
const { rmSync } = __require("fs");
|
|
341
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
342
|
+
const shortName = hookName.replace("hook-", "");
|
|
343
|
+
const hooksDir = join(targetDir, ".hooks");
|
|
344
|
+
const hookPath = join(hooksDir, hookName);
|
|
345
|
+
if (!existsSync(hookPath)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
rmSync(hookPath, { recursive: true });
|
|
349
|
+
unregisterHookFromSettings(shortName);
|
|
350
|
+
updateHooksIndex(hooksDir);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
export {
|
|
354
|
+
searchHooks,
|
|
355
|
+
removeHook,
|
|
356
|
+
installHooks,
|
|
357
|
+
installHook,
|
|
358
|
+
hookExists,
|
|
359
|
+
getRegisteredHooks,
|
|
360
|
+
getInstalledHooks,
|
|
361
|
+
getHooksByCategory,
|
|
362
|
+
getHookPath,
|
|
363
|
+
getHook,
|
|
364
|
+
HOOKS,
|
|
365
|
+
CATEGORIES
|
|
366
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for hook-agentmessages
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* hook-agentmessages install - Install hooks into Claude Code
|
|
7
|
+
* hook-agentmessages uninstall - Remove hooks from Claude Code
|
|
8
|
+
* hook-agentmessages status - Show hook status
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
const CLAUDE_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const command = args[0];
|
|
18
|
+
|
|
19
|
+
async function showStatus() {
|
|
20
|
+
console.log('hook-agentmessages status\n');
|
|
21
|
+
|
|
22
|
+
// Check if hooks are installed
|
|
23
|
+
try {
|
|
24
|
+
const file = Bun.file(CLAUDE_SETTINGS_FILE);
|
|
25
|
+
if (await file.exists()) {
|
|
26
|
+
const settings = await file.json();
|
|
27
|
+
|
|
28
|
+
if (settings.hooks) {
|
|
29
|
+
let installed = false;
|
|
30
|
+
|
|
31
|
+
if (settings.hooks.SessionStart) {
|
|
32
|
+
const hasHook = settings.hooks.SessionStart.some((h: any) =>
|
|
33
|
+
h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
|
|
34
|
+
);
|
|
35
|
+
if (hasHook) {
|
|
36
|
+
console.log(' SessionStart hook: installed');
|
|
37
|
+
installed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (settings.hooks.Stop) {
|
|
42
|
+
const hasHook = settings.hooks.Stop.some((h: any) =>
|
|
43
|
+
h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
|
|
44
|
+
);
|
|
45
|
+
if (hasHook) {
|
|
46
|
+
console.log(' Stop hook: installed');
|
|
47
|
+
installed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!installed) {
|
|
52
|
+
console.log(' No hook-agentmessages hooks installed');
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.log(' No hooks configured in Claude Code');
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(' Claude Code settings not found');
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.log(' Error reading settings:', (err as Error).message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check service-message status
|
|
65
|
+
const serviceDir = join(homedir(), '.service', 'service-message');
|
|
66
|
+
const configFile = Bun.file(join(serviceDir, 'config.json'));
|
|
67
|
+
|
|
68
|
+
console.log('\nservice-message integration:');
|
|
69
|
+
if (await configFile.exists()) {
|
|
70
|
+
const config = await configFile.json();
|
|
71
|
+
console.log(` Agent ID: ${config.agentId || 'not set'}`);
|
|
72
|
+
console.log(` Data dir: ${serviceDir}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(' Not configured');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showHelp() {
|
|
79
|
+
console.log(`
|
|
80
|
+
hook-agentmessages - Claude Code hook for service-message integration
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
hook-agentmessages <command>
|
|
84
|
+
|
|
85
|
+
Commands:
|
|
86
|
+
install Install hooks into Claude Code settings
|
|
87
|
+
uninstall Remove hooks from Claude Code settings
|
|
88
|
+
status Show current hook status
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
hook-agentmessages install
|
|
92
|
+
hook-agentmessages status
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
if (!command || command === '--help' || command === '-h') {
|
|
98
|
+
showHelp();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (command) {
|
|
103
|
+
case 'install':
|
|
104
|
+
await import('../src/install.ts');
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'uninstall':
|
|
108
|
+
await import('../src/uninstall.ts');
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'status':
|
|
112
|
+
await showStatus();
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
showHelp();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.error('Error:', err.message);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Open source Claude Code hooks library - Install hooks with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"hooks": "
|
|
7
|
+
"hooks": "bin/index.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Stop hook for service-message
|
|
4
|
-
*
|
|
5
|
-
* Runs when Claude finishes a response. Checks for unread messages
|
|
6
|
-
* and notifies Claude if there are any pending messages.
|
|
7
|
-
*
|
|
8
|
-
* This is efficient because it only runs after Claude completes a turn,
|
|
9
|
-
* not continuously polling.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { homedir } from 'os';
|
|
13
|
-
import { join } from 'path';
|
|
14
|
-
import { readdir } from 'fs/promises';
|
|
15
|
-
|
|
16
|
-
interface HookInput {
|
|
17
|
-
session_id: string;
|
|
18
|
-
cwd: string;
|
|
19
|
-
hook_event_name: string;
|
|
20
|
-
stop_hook_active?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface Message {
|
|
24
|
-
id: string;
|
|
25
|
-
timestamp: number;
|
|
26
|
-
from: string;
|
|
27
|
-
to: string;
|
|
28
|
-
project: string;
|
|
29
|
-
subject: string;
|
|
30
|
-
body: string;
|
|
31
|
-
read?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const SERVICE_DIR = join(homedir(), '.service', 'service-message');
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Sanitize ID to prevent path traversal attacks
|
|
38
|
-
*/
|
|
39
|
-
function sanitizeId(id: string): string | null {
|
|
40
|
-
if (!id || typeof id !== 'string') return null;
|
|
41
|
-
// Only allow alphanumeric, dash, underscore
|
|
42
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(id)) return null;
|
|
43
|
-
// Reject path traversal attempts
|
|
44
|
-
if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
|
|
45
|
-
return id;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function readJson<T>(path: string): Promise<T | null> {
|
|
49
|
-
try {
|
|
50
|
-
const file = Bun.file(path);
|
|
51
|
-
if (await file.exists()) {
|
|
52
|
-
return await file.json();
|
|
53
|
-
}
|
|
54
|
-
} catch {}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function getUnreadMessages(agentId: string, projectId?: string): Promise<Message[]> {
|
|
59
|
-
const messages: Message[] = [];
|
|
60
|
-
const messagesDir = join(SERVICE_DIR, 'messages');
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const rawProjects = projectId ? [projectId] : await readdir(messagesDir);
|
|
64
|
-
// Sanitize all project names to prevent path traversal
|
|
65
|
-
const projects = rawProjects.map(p => sanitizeId(p)).filter((p): p is string => p !== null);
|
|
66
|
-
|
|
67
|
-
for (const proj of projects) {
|
|
68
|
-
// Check inbox
|
|
69
|
-
const inboxDir = join(messagesDir, proj, 'inbox', agentId);
|
|
70
|
-
try {
|
|
71
|
-
const files = await readdir(inboxDir);
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
if (!file.endsWith('.json')) continue;
|
|
74
|
-
// Sanitize filename to prevent path traversal
|
|
75
|
-
const safeFile = sanitizeId(file.replace('.json', ''));
|
|
76
|
-
if (!safeFile) continue;
|
|
77
|
-
const msg = await readJson<Message>(join(inboxDir, `${safeFile}.json`));
|
|
78
|
-
if (msg && !msg.read) {
|
|
79
|
-
messages.push(msg);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch {}
|
|
83
|
-
|
|
84
|
-
// Check broadcast
|
|
85
|
-
const broadcastDir = join(messagesDir, proj, 'broadcast');
|
|
86
|
-
try {
|
|
87
|
-
const files = await readdir(broadcastDir);
|
|
88
|
-
for (const file of files) {
|
|
89
|
-
if (!file.endsWith('.json')) continue;
|
|
90
|
-
// Sanitize filename to prevent path traversal
|
|
91
|
-
const safeFile = sanitizeId(file.replace('.json', ''));
|
|
92
|
-
if (!safeFile) continue;
|
|
93
|
-
const msg = await readJson<Message>(join(broadcastDir, `${safeFile}.json`));
|
|
94
|
-
if (msg && !msg.read && msg.from !== agentId) {
|
|
95
|
-
messages.push(msg);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch {}
|
|
99
|
-
}
|
|
100
|
-
} catch {}
|
|
101
|
-
|
|
102
|
-
return messages.sort((a, b) => b.timestamp - a.timestamp);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function main() {
|
|
106
|
-
// Get agent ID from environment (set by session-start hook)
|
|
107
|
-
const rawAgentId = process.env.SMSG_AGENT_ID;
|
|
108
|
-
const rawProjectId = process.env.SMSG_PROJECT_ID;
|
|
109
|
-
|
|
110
|
-
// Sanitize IDs to prevent path traversal
|
|
111
|
-
const agentId = rawAgentId ? sanitizeId(rawAgentId) : null;
|
|
112
|
-
const projectId = rawProjectId ? sanitizeId(rawProjectId) : undefined;
|
|
113
|
-
|
|
114
|
-
if (!agentId) {
|
|
115
|
-
// Agent not configured or invalid, skip silently
|
|
116
|
-
console.log(JSON.stringify({ continue: true }));
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Check for unread messages
|
|
121
|
-
const unreadMessages = await getUnreadMessages(agentId, projectId);
|
|
122
|
-
|
|
123
|
-
if (unreadMessages.length === 0) {
|
|
124
|
-
// No messages, continue normally
|
|
125
|
-
console.log(JSON.stringify({ continue: true }));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Format message summary in a friendly way
|
|
130
|
-
const msgList = unreadMessages.slice(0, 3).map(msg => {
|
|
131
|
-
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
132
|
-
const preview = msg.body.slice(0, 60).replace(/\n/g, ' ');
|
|
133
|
-
return `šØ **${msg.subject}** from \`${msg.from}\` (${time})\n ${preview}${msg.body.length > 60 ? '...' : ''}`;
|
|
134
|
-
}).join('\n\n');
|
|
135
|
-
|
|
136
|
-
const moreNote = unreadMessages.length > 3
|
|
137
|
-
? `\n\n_...and ${unreadMessages.length - 3} more message(s)_`
|
|
138
|
-
: '';
|
|
139
|
-
|
|
140
|
-
// Inject message in a friendly, readable format
|
|
141
|
-
console.log(JSON.stringify({
|
|
142
|
-
continue: true,
|
|
143
|
-
stopReason: `š¬ **You have ${unreadMessages.length} unread message(s):**\n\n${msgList}${moreNote}\n\nš” Use \`service-message inbox\` to see all messages or \`service-message read <id>\` to read one.`
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
main().catch(() => {
|
|
148
|
-
// Don't block on errors
|
|
149
|
-
console.log(JSON.stringify({ continue: true }));
|
|
150
|
-
process.exit(0);
|
|
151
|
-
});
|