@hasna/hooks 0.0.1
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/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- 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/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -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-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: check-tasks
|
|
5
|
+
*
|
|
6
|
+
* Prevents Claude from stopping when there are pending/in-progress tasks.
|
|
7
|
+
*
|
|
8
|
+
* Configuration priority:
|
|
9
|
+
* 1. settings.json checkTasksConfig (project or global)
|
|
10
|
+
* 2. Environment variables (legacy)
|
|
11
|
+
*
|
|
12
|
+
* Config options:
|
|
13
|
+
* - taskListId: specific list to check, or undefined = check all lists
|
|
14
|
+
* - keywords: keywords that trigger the check (default: ["dev"])
|
|
15
|
+
* - enabled: enable/disable the hook
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
|
|
22
|
+
interface Task {
|
|
23
|
+
id: string;
|
|
24
|
+
subject: string;
|
|
25
|
+
status: "pending" | "in_progress" | "completed";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CheckTasksConfig {
|
|
29
|
+
taskListId?: string;
|
|
30
|
+
keywords?: string[];
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HookInput {
|
|
35
|
+
session_id: string;
|
|
36
|
+
transcript_path: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
permission_mode: string;
|
|
39
|
+
hook_event_name: string;
|
|
40
|
+
stop_hook_active: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CONFIG_KEY = "checkTasksConfig";
|
|
44
|
+
|
|
45
|
+
function readStdinJson(): HookInput | null {
|
|
46
|
+
try {
|
|
47
|
+
const stdin = readFileSync(0, "utf-8");
|
|
48
|
+
return JSON.parse(stdin);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readSettings(path: string): Record<string, unknown> {
|
|
55
|
+
if (!existsSync(path)) return {};
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getConfig(cwd: string): CheckTasksConfig {
|
|
64
|
+
// Try project settings first
|
|
65
|
+
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
66
|
+
if (projectSettings[CONFIG_KEY]) {
|
|
67
|
+
return projectSettings[CONFIG_KEY] as CheckTasksConfig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fall back to global settings
|
|
71
|
+
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
72
|
+
if (globalSettings[CONFIG_KEY]) {
|
|
73
|
+
return globalSettings[CONFIG_KEY] as CheckTasksConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Legacy: use environment variables
|
|
77
|
+
return {
|
|
78
|
+
taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
|
|
79
|
+
keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
|
|
80
|
+
enabled: process.env.CHECK_TASKS_DISABLED !== "1",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSessionName(transcriptPath: string): string | null {
|
|
85
|
+
if (!existsSync(transcriptPath)) return null;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const content = readFileSync(transcriptPath, "utf-8");
|
|
89
|
+
let lastTitle: string | null = null;
|
|
90
|
+
let searchStart = 0;
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
94
|
+
if (titleIndex === -1) break;
|
|
95
|
+
|
|
96
|
+
const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
|
|
97
|
+
const lineEnd = content.indexOf("\n", titleIndex);
|
|
98
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const entry = JSON.parse(line);
|
|
102
|
+
if (entry.type === "custom-title" && entry.customTitle) {
|
|
103
|
+
lastTitle = entry.customTitle;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip malformed lines
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
searchStart = titleIndex + 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lastTitle;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getAllTaskLists(): string[] {
|
|
119
|
+
const tasksDir = join(homedir(), ".claude", "tasks");
|
|
120
|
+
if (!existsSync(tasksDir)) return [];
|
|
121
|
+
try {
|
|
122
|
+
return readdirSync(tasksDir, { withFileTypes: true })
|
|
123
|
+
.filter((d) => d.isDirectory())
|
|
124
|
+
.map((d) => d.name);
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getProjectTaskLists(cwd: string): string[] {
|
|
131
|
+
const allLists = getAllTaskLists();
|
|
132
|
+
|
|
133
|
+
// Get the directory name as project identifier
|
|
134
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
135
|
+
|
|
136
|
+
// Filter lists that match the project name
|
|
137
|
+
const projectLists = allLists.filter((list) => {
|
|
138
|
+
const listLower = list.toLowerCase();
|
|
139
|
+
const dirLower = dirName.toLowerCase();
|
|
140
|
+
|
|
141
|
+
// Exact prefix match (e.g., "connect-x" matches "connect-x-dev")
|
|
142
|
+
if (listLower.startsWith(dirLower + "-")) return true;
|
|
143
|
+
|
|
144
|
+
// Exact match
|
|
145
|
+
if (listLower === dirLower) return true;
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return projectLists;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getTasksFromList(listId: string): Task[] {
|
|
154
|
+
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
155
|
+
if (!existsSync(tasksDir)) return [];
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
159
|
+
return taskFiles.map((file) => {
|
|
160
|
+
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
161
|
+
return JSON.parse(content) as Task;
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function approve() {
|
|
169
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function block(reason: string) {
|
|
174
|
+
console.log(JSON.stringify({ decision: "block", reason }));
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function run() {
|
|
179
|
+
const hookInput = readStdinJson();
|
|
180
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
181
|
+
|
|
182
|
+
const config = getConfig(cwd);
|
|
183
|
+
|
|
184
|
+
// Check if hook is disabled
|
|
185
|
+
if (config.enabled === false) {
|
|
186
|
+
approve();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get session name from transcript
|
|
190
|
+
let sessionName: string | null = null;
|
|
191
|
+
if (hookInput?.transcript_path) {
|
|
192
|
+
sessionName = getSessionName(hookInput.transcript_path);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Determine what to check for keywords
|
|
196
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
197
|
+
const keywords = config.keywords || ["dev"];
|
|
198
|
+
|
|
199
|
+
// Only block stop for sessions matching configured keywords
|
|
200
|
+
const matchesKeyword = keywords.some((keyword) =>
|
|
201
|
+
nameToCheck.toLowerCase().includes(keyword.toLowerCase())
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
205
|
+
// Not a matching session, allow stop
|
|
206
|
+
approve();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Determine which task lists to check
|
|
210
|
+
let listsToCheck: string[] = [];
|
|
211
|
+
|
|
212
|
+
if (config.taskListId) {
|
|
213
|
+
// Specific list configured
|
|
214
|
+
listsToCheck = [config.taskListId];
|
|
215
|
+
} else {
|
|
216
|
+
// Get lists that belong to this project/folder
|
|
217
|
+
const projectLists = getProjectTaskLists(cwd);
|
|
218
|
+
|
|
219
|
+
if (projectLists.length > 0) {
|
|
220
|
+
// Filter by keywords if specified
|
|
221
|
+
if (keywords.length > 0) {
|
|
222
|
+
listsToCheck = projectLists.filter((list) =>
|
|
223
|
+
keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase()))
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
listsToCheck = projectLists;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// If no project-specific lists found, don't check any (don't fall back to all lists)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (listsToCheck.length === 0) {
|
|
233
|
+
// No matching task lists, allow stop
|
|
234
|
+
approve();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Collect tasks from all matching lists
|
|
238
|
+
let allPending: Task[] = [];
|
|
239
|
+
let allInProgress: Task[] = [];
|
|
240
|
+
let allCompleted: Task[] = [];
|
|
241
|
+
let activeListId: string | null = null;
|
|
242
|
+
|
|
243
|
+
for (const listId of listsToCheck) {
|
|
244
|
+
const tasks = getTasksFromList(listId);
|
|
245
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
246
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
247
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
248
|
+
|
|
249
|
+
if (pending.length > 0 || inProgress.length > 0) {
|
|
250
|
+
activeListId = listId;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
allPending.push(...pending);
|
|
254
|
+
allInProgress.push(...inProgress);
|
|
255
|
+
allCompleted.push(...completed);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const remainingCount = allPending.length + allInProgress.length;
|
|
259
|
+
|
|
260
|
+
if (remainingCount > 0) {
|
|
261
|
+
const nextTasks = allPending
|
|
262
|
+
.slice(0, 3)
|
|
263
|
+
.map((t) => `- ${t.subject}`)
|
|
264
|
+
.join("\n");
|
|
265
|
+
|
|
266
|
+
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
267
|
+
|
|
268
|
+
const prompt = `
|
|
269
|
+
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
270
|
+
|
|
271
|
+
⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
|
|
272
|
+
|
|
273
|
+
You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
|
|
274
|
+
|
|
275
|
+
Next pending tasks:
|
|
276
|
+
${nextTasks}
|
|
277
|
+
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
278
|
+
|
|
279
|
+
MANDATORY INSTRUCTIONS (follow these NOW):
|
|
280
|
+
1. Use TaskList to see all tasks
|
|
281
|
+
2. Use TaskGet to read the FIRST pending task's full description
|
|
282
|
+
3. Use TaskUpdate to mark it as in_progress BEFORE starting work
|
|
283
|
+
4. Complete the task (write code, run commands, etc.)
|
|
284
|
+
5. Use TaskUpdate to mark it as completed AFTER finishing
|
|
285
|
+
6. IMMEDIATELY move to the next task - DO NOT STOP
|
|
286
|
+
|
|
287
|
+
CRITICAL RULES:
|
|
288
|
+
- NEVER ask "would you like me to..." - just DO IT
|
|
289
|
+
- NEVER ask for confirmation - just WORK
|
|
290
|
+
- NEVER stop to explain what you'll do - just DO IT
|
|
291
|
+
- If a task is unclear, make reasonable assumptions and proceed
|
|
292
|
+
- If you encounter an error, fix it and continue
|
|
293
|
+
- Keep working until remainingCount = 0
|
|
294
|
+
|
|
295
|
+
START WORKING NOW. Use TaskList tool in your next response.
|
|
296
|
+
`.trim();
|
|
297
|
+
|
|
298
|
+
block(prompt);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// All tasks completed, allow stop
|
|
302
|
+
approve();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Allow direct execution
|
|
306
|
+
if (import.meta.main) {
|
|
307
|
+
run();
|
|
308
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"types": ["bun-types", "node"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hasna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @hasnaxyz/hook-checktests
|
|
2
|
+
|
|
3
|
+
Claude Code hook that checks for missing tests via a headless Claude agent. Runs async (non-blocking) on PostToolUse after file edits.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Edit tracking**: Monitors Edit, Write, NotebookEdit tools
|
|
8
|
+
- **Configurable threshold**: Run after N edits (3-7, default: 3)
|
|
9
|
+
- **Headless review**: Spawns Claude agent to analyze test coverage
|
|
10
|
+
- **Task dispatch**: Creates tasks via `service-implementation task dispatch`
|
|
11
|
+
- **Repo pattern check**: Only runs for repos matching `[prefix]-[name]` pattern
|
|
12
|
+
- **Session-aware**: Only runs for sessions matching configured keywords
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Global CLI
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add -g @hasnaxyz/hook-checktests
|
|
20
|
+
hook-checktests install --global
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Project-specific
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd /path/to/your/project
|
|
27
|
+
bunx @hasnaxyz/hook-checktests install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- `claude` CLI (for headless agent)
|
|
33
|
+
- `service-implementation` CLI (for task dispatch)
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Once installed, the hook runs automatically after file edits.
|
|
38
|
+
|
|
39
|
+
### Commands
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
hook-checktests install [path] # Install the hook
|
|
43
|
+
hook-checktests config [path] # Update configuration
|
|
44
|
+
hook-checktests uninstall [path] # Remove the hook
|
|
45
|
+
hook-checktests status # Show hook status
|
|
46
|
+
hook-checktests run # Execute hook (called by Claude Code)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
- `--global`, `-g`: Apply to global settings (`~/.claude/settings.json`)
|
|
52
|
+
- `/path/to/repo`: Apply to specific project path
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
Configuration is stored in `.claude/settings.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"hooks": {
|
|
61
|
+
"PostToolUse": [{
|
|
62
|
+
"matcher": { "tool_name": "^(Edit|Write|NotebookEdit)$" },
|
|
63
|
+
"hooks": [{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "bunx @hasnaxyz/hook-checktests@latest run",
|
|
66
|
+
"timeout": 120,
|
|
67
|
+
"async": true
|
|
68
|
+
}]
|
|
69
|
+
}]
|
|
70
|
+
},
|
|
71
|
+
"checkTestsConfig": {
|
|
72
|
+
"editThreshold": 3,
|
|
73
|
+
"taskListId": "myproject-qa",
|
|
74
|
+
"keywords": ["dev"],
|
|
75
|
+
"enabled": true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Options
|
|
81
|
+
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
|--------|------|---------|-------------|
|
|
84
|
+
| `editThreshold` | number | 3 | Run review after this many edits (3-7) |
|
|
85
|
+
| `taskListId` | string | auto | Task list for dispatching tasks (auto-detects `*-qa`) |
|
|
86
|
+
| `keywords` | string[] | ["dev"] | Only run for matching sessions |
|
|
87
|
+
| `enabled` | boolean | true | Enable/disable the hook |
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
1. **Tracks edits**: Monitors Edit, Write, NotebookEdit tool calls
|
|
92
|
+
2. **Counts edits**: Increments counter for each unique file edited
|
|
93
|
+
3. **Threshold check**: After N edits, spawns headless Claude agent
|
|
94
|
+
4. **Test review**: Agent analyzes edited files for missing tests
|
|
95
|
+
5. **Task dispatch**: Creates tasks via `service-implementation task dispatch`
|
|
96
|
+
6. **Reset**: Counter resets after each review
|
|
97
|
+
|
|
98
|
+
## Test Issues Detected
|
|
99
|
+
|
|
100
|
+
The hook checks for:
|
|
101
|
+
|
|
102
|
+
- Missing unit tests for new functions/methods
|
|
103
|
+
- Missing integration tests for new features
|
|
104
|
+
- Missing edge case tests
|
|
105
|
+
- Missing error handling tests
|
|
106
|
+
- Untested code paths
|
|
107
|
+
- Missing mock/stub implementations
|
|
108
|
+
- Missing test fixtures or setup
|
|
109
|
+
- Missing API endpoint tests
|
|
110
|
+
- Missing validation tests
|
|
111
|
+
|
|
112
|
+
## Task Format
|
|
113
|
+
|
|
114
|
+
Tasks are dispatched with:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
service-implementation task dispatch "myproject-qa" \
|
|
118
|
+
-s "TEST: [brief description]" \
|
|
119
|
+
-d "[detailed description of what tests need to be added]"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Session State
|
|
123
|
+
|
|
124
|
+
State is persisted in `~/.claude/hook-state/checktests-{session_id}.json`:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"editCount": 2,
|
|
129
|
+
"editedFiles": ["src/utils.ts", "src/api.ts"],
|
|
130
|
+
"lastCheckRun": 1706500000000,
|
|
131
|
+
"checkInProgress": false
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasnaxyz/hook-checktests",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Claude Code hook that checks for missing tests and creates tasks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hook-checktests": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/hook.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/hook.js",
|
|
13
|
+
"types": "./dist/hook.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./cli": {
|
|
16
|
+
"import": "./dist/cli.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build ./src/cli.ts ./src/hook.ts --outdir ./dist --target node",
|
|
25
|
+
"prepublishOnly": "bun run build",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"claude-code",
|
|
30
|
+
"claude",
|
|
31
|
+
"hook",
|
|
32
|
+
"tests",
|
|
33
|
+
"testing",
|
|
34
|
+
"headless",
|
|
35
|
+
"automation",
|
|
36
|
+
"cli"
|
|
37
|
+
],
|
|
38
|
+
"author": "Hasna",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/hasnaxyz/hook-checktests.git"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "restricted",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18",
|
|
50
|
+
"bun": ">=1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "^1.3.8",
|
|
54
|
+
"@types/node": "^20",
|
|
55
|
+
"typescript": "^5.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|