@hasna/hooks 0.0.4 → 0.0.6

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.
Files changed (37) hide show
  1. package/bin/index.js +429 -133
  2. package/dist/index.js +46 -90
  3. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  4. package/hooks/hook-agentmessages/src/install.ts +126 -0
  5. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  6. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  7. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  8. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  9. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  10. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  11. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  12. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  13. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  14. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  15. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  16. package/hooks/hook-checklint/src/cli.ts +667 -0
  17. package/hooks/hook-checklint/src/hook.ts +473 -0
  18. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  19. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  20. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  21. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  22. package/hooks/hook-checktasks/src/cli.ts +578 -0
  23. package/hooks/hook-checktasks/src/hook.ts +308 -0
  24. package/hooks/hook-checktests/src/cli.ts +627 -0
  25. package/hooks/hook-checktests/src/hook.ts +334 -0
  26. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  27. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  28. package/hooks/hook-gitguard/src/cli.ts +159 -0
  29. package/hooks/hook-gitguard/src/hook.ts +129 -0
  30. package/hooks/hook-packageage/src/cli.ts +165 -0
  31. package/hooks/hook-packageage/src/hook.ts +177 -0
  32. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  33. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  34. package/hooks/hook-precompact/src/cli.ts +168 -0
  35. package/hooks/hook-precompact/src/hook.ts +122 -0
  36. package/package.json +2 -1
  37. package/.hooks/index.ts +0 -6
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __require = import.meta.require;
3
-
4
2
  // src/lib/registry.ts
5
3
  var CATEGORIES = [
6
4
  "Git Safety",
@@ -172,13 +170,18 @@ function getHook(name) {
172
170
  return HOOKS.find((h) => h.name === name);
173
171
  }
174
172
  // src/lib/installer.ts
175
- import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
173
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
176
174
  import { join, dirname } from "path";
177
175
  import { homedir } from "os";
178
176
  import { fileURLToPath } from "url";
179
177
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
180
178
  var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
181
- var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
179
+ function getSettingsPath(scope = "global") {
180
+ if (scope === "project") {
181
+ return join(process.cwd(), ".claude", "settings.json");
182
+ }
183
+ return join(homedir(), ".claude", "settings.json");
184
+ }
182
185
  function getHookPath(name) {
183
186
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
184
187
  return join(HOOKS_DIR, hookName);
@@ -186,56 +189,38 @@ function getHookPath(name) {
186
189
  function hookExists(name) {
187
190
  return existsSync(getHookPath(name));
188
191
  }
189
- function readSettings() {
192
+ function readSettings(scope = "global") {
193
+ const path = getSettingsPath(scope);
190
194
  try {
191
- if (existsSync(SETTINGS_PATH)) {
192
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
195
+ if (existsSync(path)) {
196
+ return JSON.parse(readFileSync(path, "utf-8"));
193
197
  }
194
198
  } catch {}
195
199
  return {};
196
200
  }
197
- function writeSettings(settings) {
198
- const dir = dirname(SETTINGS_PATH);
201
+ function writeSettings(settings, scope = "global") {
202
+ const path = getSettingsPath(scope);
203
+ const dir = dirname(path);
199
204
  if (!existsSync(dir)) {
200
205
  mkdirSync(dir, { recursive: true });
201
206
  }
202
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
207
+ writeFileSync(path, JSON.stringify(settings, null, 2) + `
203
208
  `);
204
209
  }
205
210
  function installHook(name, options = {}) {
206
- const { targetDir = process.cwd(), overwrite = false } = options;
211
+ const { scope = "global", overwrite = false } = options;
207
212
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
208
213
  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
- };
214
+ if (!hookExists(shortName)) {
215
+ return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
218
216
  }
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
- };
217
+ const registered = getRegisteredHooks(scope);
218
+ if (registered.includes(shortName) && !overwrite) {
219
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
226
220
  }
227
221
  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
- };
222
+ registerHook(shortName, scope);
223
+ return { hook: shortName, success: true, scope };
239
224
  } catch (error) {
240
225
  return {
241
226
  hook: shortName,
@@ -244,20 +229,18 @@ function installHook(name, options = {}) {
244
229
  };
245
230
  }
246
231
  }
247
- function registerHookInSettings(name) {
232
+ function registerHook(name, scope = "global") {
248
233
  const meta = getHook(name);
249
234
  if (!meta)
250
235
  return;
251
- const settings = readSettings();
236
+ const settings = readSettings(scope);
252
237
  if (!settings.hooks)
253
238
  settings.hooks = {};
254
239
  const eventKey = meta.event;
255
240
  if (!settings.hooks[eventKey])
256
241
  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;
242
+ const hookCommand = `hooks run ${name}`;
243
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
261
244
  const entry = {
262
245
  hooks: [{ type: "command", command: hookCommand }]
263
246
  };
@@ -265,69 +248,42 @@ function registerHookInSettings(name) {
265
248
  entry.matcher = meta.matcher;
266
249
  }
267
250
  settings.hooks[eventKey].push(entry);
268
- writeSettings(settings);
251
+ writeSettings(settings, scope);
269
252
  }
270
- function unregisterHookFromSettings(name) {
253
+ function unregisterHook(name, scope = "global") {
271
254
  const meta = getHook(name);
272
255
  if (!meta)
273
256
  return;
274
- const settings = readSettings();
257
+ const settings = readSettings(scope);
275
258
  if (!settings.hooks)
276
259
  return;
277
260
  const eventKey = meta.event;
278
261
  if (!settings.hooks[eventKey])
279
262
  return;
280
- const hookCommand = `hook-${name}`;
281
- settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
263
+ const hookCommand = `hooks run ${name}`;
264
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
282
265
  if (settings.hooks[eventKey].length === 0) {
283
266
  delete settings.hooks[eventKey];
284
267
  }
285
268
  if (Object.keys(settings.hooks).length === 0) {
286
269
  delete settings.hooks;
287
270
  }
288
- writeSettings(settings);
271
+ writeSettings(settings, scope);
289
272
  }
290
273
  function installHooks(names, options = {}) {
291
274
  return names.map((name) => installHook(name, options));
292
275
  }
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();
276
+ function getRegisteredHooks(scope = "global") {
277
+ const settings = readSettings(scope);
324
278
  if (!settings.hooks)
325
279
  return [];
326
280
  const registered = [];
327
281
  for (const eventKey of Object.keys(settings.hooks)) {
328
282
  for (const entry of settings.hooks[eventKey]) {
329
283
  for (const hook of entry.hooks || []) {
330
- const match = hook.command?.match(/hook-(\w+)/);
284
+ const newMatch = hook.command?.match(/^hooks run (\w+)$/);
285
+ const oldMatch = hook.command?.match(/^hook-(\w+)$/);
286
+ const match = newMatch || oldMatch;
331
287
  if (match) {
332
288
  registered.push(match[1]);
333
289
  }
@@ -336,18 +292,17 @@ function getRegisteredHooks() {
336
292
  }
337
293
  return [...new Set(registered)];
338
294
  }
339
- function removeHook(name, targetDir = process.cwd()) {
340
- const { rmSync } = __require("fs");
295
+ function getInstalledHooks(scope = "global") {
296
+ return getRegisteredHooks(scope);
297
+ }
298
+ function removeHook(name, scope = "global") {
341
299
  const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
342
300
  const shortName = hookName.replace("hook-", "");
343
- const hooksDir = join(targetDir, ".hooks");
344
- const hookPath = join(hooksDir, hookName);
345
- if (!existsSync(hookPath)) {
301
+ const registered = getRegisteredHooks(scope);
302
+ if (!registered.includes(shortName)) {
346
303
  return false;
347
304
  }
348
- rmSync(hookPath, { recursive: true });
349
- unregisterHookFromSettings(shortName);
350
- updateHooksIndex(hooksDir);
305
+ unregisterHook(shortName, scope);
351
306
  return true;
352
307
  }
353
308
  export {
@@ -356,6 +311,7 @@ export {
356
311
  installHooks,
357
312
  installHook,
358
313
  hookExists,
314
+ getSettingsPath,
359
315
  getRegisteredHooks,
360
316
  getInstalledHooks,
361
317
  getHooksByCategory,
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Stop hook for service-message
4
+ *
5
+ * Runs when Claude finishes a response. Checks for unread messages
6
+ * and notifies Claude if there are any pending messages.
7
+ *
8
+ * This is efficient because it only runs after Claude completes a turn,
9
+ * not continuously polling.
10
+ */
11
+
12
+ import { homedir } from 'os';
13
+ import { join } from 'path';
14
+ import { readdir } from 'fs/promises';
15
+
16
+ interface HookInput {
17
+ session_id: string;
18
+ cwd: string;
19
+ hook_event_name: string;
20
+ stop_hook_active?: boolean;
21
+ }
22
+
23
+ interface Message {
24
+ id: string;
25
+ timestamp: number;
26
+ from: string;
27
+ to: string;
28
+ project: string;
29
+ subject: string;
30
+ body: string;
31
+ read?: boolean;
32
+ }
33
+
34
+ const SERVICE_DIR = join(homedir(), '.service', 'service-message');
35
+
36
+ /**
37
+ * Sanitize ID to prevent path traversal attacks
38
+ */
39
+ function sanitizeId(id: string): string | null {
40
+ if (!id || typeof id !== 'string') return null;
41
+ // Only allow alphanumeric, dash, underscore
42
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) return null;
43
+ // Reject path traversal attempts
44
+ if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
45
+ return id;
46
+ }
47
+
48
+ async function readJson<T>(path: string): Promise<T | null> {
49
+ try {
50
+ const file = Bun.file(path);
51
+ if (await file.exists()) {
52
+ return await file.json();
53
+ }
54
+ } catch {}
55
+ return null;
56
+ }
57
+
58
+ async function getUnreadMessages(agentId: string, projectId?: string): Promise<Message[]> {
59
+ const messages: Message[] = [];
60
+ const messagesDir = join(SERVICE_DIR, 'messages');
61
+
62
+ try {
63
+ const rawProjects = projectId ? [projectId] : await readdir(messagesDir);
64
+ // Sanitize all project names to prevent path traversal
65
+ const projects = rawProjects.map(p => sanitizeId(p)).filter((p): p is string => p !== null);
66
+
67
+ for (const proj of projects) {
68
+ // Check inbox
69
+ const inboxDir = join(messagesDir, proj, 'inbox', agentId);
70
+ try {
71
+ const files = await readdir(inboxDir);
72
+ for (const file of files) {
73
+ if (!file.endsWith('.json')) continue;
74
+ // Sanitize filename to prevent path traversal
75
+ const safeFile = sanitizeId(file.replace('.json', ''));
76
+ if (!safeFile) continue;
77
+ const msg = await readJson<Message>(join(inboxDir, `${safeFile}.json`));
78
+ if (msg && !msg.read) {
79
+ messages.push(msg);
80
+ }
81
+ }
82
+ } catch {}
83
+
84
+ // Check broadcast
85
+ const broadcastDir = join(messagesDir, proj, 'broadcast');
86
+ try {
87
+ const files = await readdir(broadcastDir);
88
+ for (const file of files) {
89
+ if (!file.endsWith('.json')) continue;
90
+ // Sanitize filename to prevent path traversal
91
+ const safeFile = sanitizeId(file.replace('.json', ''));
92
+ if (!safeFile) continue;
93
+ const msg = await readJson<Message>(join(broadcastDir, `${safeFile}.json`));
94
+ if (msg && !msg.read && msg.from !== agentId) {
95
+ messages.push(msg);
96
+ }
97
+ }
98
+ } catch {}
99
+ }
100
+ } catch {}
101
+
102
+ return messages.sort((a, b) => b.timestamp - a.timestamp);
103
+ }
104
+
105
+ async function main() {
106
+ // Get agent ID from environment (set by session-start hook)
107
+ const rawAgentId = process.env.SMSG_AGENT_ID;
108
+ const rawProjectId = process.env.SMSG_PROJECT_ID;
109
+
110
+ // Sanitize IDs to prevent path traversal
111
+ const agentId = rawAgentId ? sanitizeId(rawAgentId) : null;
112
+ const projectId = rawProjectId ? sanitizeId(rawProjectId) : undefined;
113
+
114
+ if (!agentId) {
115
+ // Agent not configured or invalid, skip silently
116
+ console.log(JSON.stringify({ continue: true }));
117
+ return;
118
+ }
119
+
120
+ // Check for unread messages
121
+ const unreadMessages = await getUnreadMessages(agentId, projectId);
122
+
123
+ if (unreadMessages.length === 0) {
124
+ // No messages, continue normally
125
+ console.log(JSON.stringify({ continue: true }));
126
+ return;
127
+ }
128
+
129
+ // Format message summary in a friendly way
130
+ const msgList = unreadMessages.slice(0, 3).map(msg => {
131
+ const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
132
+ const preview = msg.body.slice(0, 60).replace(/\n/g, ' ');
133
+ return `šŸ“Ø **${msg.subject}** from \`${msg.from}\` (${time})\n ${preview}${msg.body.length > 60 ? '...' : ''}`;
134
+ }).join('\n\n');
135
+
136
+ const moreNote = unreadMessages.length > 3
137
+ ? `\n\n_...and ${unreadMessages.length - 3} more message(s)_`
138
+ : '';
139
+
140
+ // Inject message in a friendly, readable format
141
+ console.log(JSON.stringify({
142
+ continue: true,
143
+ stopReason: `šŸ“¬ **You have ${unreadMessages.length} unread message(s):**\n\n${msgList}${moreNote}\n\nšŸ’” Use \`service-message inbox\` to see all messages or \`service-message read <id>\` to read one.`
144
+ }));
145
+ }
146
+
147
+ main().catch(() => {
148
+ // Don't block on errors
149
+ console.log(JSON.stringify({ continue: true }));
150
+ process.exit(0);
151
+ });
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Install hook-agentmessages into Claude Code settings
4
+ *
5
+ * Adds hooks to ~/.claude/settings.json:
6
+ * - SessionStart: Auto-register agent, project, session
7
+ * - Stop: Check for unread messages
8
+ */
9
+
10
+ import { homedir } from 'os';
11
+ import { join } from 'path';
12
+
13
+ const CLAUDE_SETTINGS_DIR = join(homedir(), '.claude');
14
+ const CLAUDE_SETTINGS_FILE = join(CLAUDE_SETTINGS_DIR, 'settings.json');
15
+ const HOOK_DIR = import.meta.dir.replace('/src', '');
16
+
17
+ interface HookConfig {
18
+ type: 'command';
19
+ command: string;
20
+ timeout?: number;
21
+ }
22
+
23
+ interface HookMatcher {
24
+ matcher?: string;
25
+ hooks: HookConfig[];
26
+ }
27
+
28
+ interface Settings {
29
+ hooks?: {
30
+ SessionStart?: HookMatcher[];
31
+ Stop?: HookMatcher[];
32
+ [key: string]: HookMatcher[] | undefined;
33
+ };
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ async function readSettings(): Promise<Settings> {
38
+ try {
39
+ const file = Bun.file(CLAUDE_SETTINGS_FILE);
40
+ if (await file.exists()) {
41
+ return await file.json();
42
+ }
43
+ } catch {}
44
+ return {};
45
+ }
46
+
47
+ async function writeSettings(settings: Settings): Promise<void> {
48
+ // Ensure .claude directory exists
49
+ await Bun.write(join(CLAUDE_SETTINGS_DIR, '.gitkeep'), '');
50
+ await Bun.write(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
51
+ }
52
+
53
+ function findHookIndex(hooks: HookMatcher[], command: string): number {
54
+ return hooks.findIndex(h =>
55
+ h.hooks.some(hook => hook.command.includes('hook-agentmessages'))
56
+ );
57
+ }
58
+
59
+ async function main() {
60
+ console.log('Installing hook-agentmessages into Claude Code...\n');
61
+
62
+ const settings = await readSettings();
63
+
64
+ // Initialize hooks object if not exists
65
+ if (!settings.hooks) {
66
+ settings.hooks = {};
67
+ }
68
+
69
+ // SessionStart hook
70
+ const sessionStartHook: HookMatcher = {
71
+ hooks: [
72
+ {
73
+ type: 'command',
74
+ command: `bun ${join(HOOK_DIR, 'src/session-start.ts')}`,
75
+ timeout: 10,
76
+ },
77
+ ],
78
+ };
79
+
80
+ if (!settings.hooks.SessionStart) {
81
+ settings.hooks.SessionStart = [];
82
+ }
83
+
84
+ // Remove existing hook-agentmessages hooks
85
+ const existingSessionStartIdx = findHookIndex(settings.hooks.SessionStart, 'hook-agentmessages');
86
+ if (existingSessionStartIdx >= 0) {
87
+ settings.hooks.SessionStart.splice(existingSessionStartIdx, 1);
88
+ }
89
+ settings.hooks.SessionStart.push(sessionStartHook);
90
+
91
+ // Stop hook (check messages after each response)
92
+ const stopHook: HookMatcher = {
93
+ hooks: [
94
+ {
95
+ type: 'command',
96
+ command: `bun ${join(HOOK_DIR, 'src/check-messages.ts')}`,
97
+ timeout: 5,
98
+ },
99
+ ],
100
+ };
101
+
102
+ if (!settings.hooks.Stop) {
103
+ settings.hooks.Stop = [];
104
+ }
105
+
106
+ const existingStopIdx = findHookIndex(settings.hooks.Stop, 'hook-agentmessages');
107
+ if (existingStopIdx >= 0) {
108
+ settings.hooks.Stop.splice(existingStopIdx, 1);
109
+ }
110
+ settings.hooks.Stop.push(stopHook);
111
+
112
+ // Write updated settings
113
+ await writeSettings(settings);
114
+
115
+ console.log('Hooks installed successfully!\n');
116
+ console.log('Installed hooks:');
117
+ console.log(' - SessionStart: Auto-registers agent, project, and session');
118
+ console.log(' - Stop: Checks for unread messages after each response\n');
119
+ console.log(`Settings file: ${CLAUDE_SETTINGS_FILE}`);
120
+ console.log('\nRestart Claude Code for hooks to take effect.');
121
+ }
122
+
123
+ main().catch((err) => {
124
+ console.error('Installation failed:', err.message);
125
+ process.exit(1);
126
+ });