@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.
- package/bin/index.js +429 -133
- package/dist/index.js +46 -90
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/package.json +2 -1
- package/.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,
|
|
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
|
-
|
|
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(
|
|
192
|
-
return JSON.parse(readFileSync(
|
|
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
|
|
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(
|
|
207
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + `
|
|
203
208
|
`);
|
|
204
209
|
}
|
|
205
210
|
function installHook(name, options = {}) {
|
|
206
|
-
const {
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
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 = `
|
|
258
|
-
|
|
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
|
|
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 = `
|
|
281
|
-
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command
|
|
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
|
|
294
|
-
const
|
|
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
|
|
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
|
|
340
|
-
|
|
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
|
|
344
|
-
|
|
345
|
-
if (!existsSync(hookPath)) {
|
|
301
|
+
const registered = getRegisteredHooks(scope);
|
|
302
|
+
if (!registered.includes(shortName)) {
|
|
346
303
|
return false;
|
|
347
304
|
}
|
|
348
|
-
|
|
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
|
+
});
|