@hasna/hooks 0.0.1 → 0.0.3

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 (64) hide show
  1. package/.hooks/index.ts +6 -0
  2. package/bin/index.js +1 -1
  3. package/dist/index.js +366 -0
  4. package/hooks/hook-agentmessages/bin/cli.ts +125 -0
  5. package/package.json +2 -2
  6. package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
  7. package/hooks/hook-agentmessages/src/install.ts +0 -126
  8. package/hooks/hook-agentmessages/src/session-start.ts +0 -255
  9. package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
  10. package/hooks/hook-branchprotect/src/cli.ts +0 -126
  11. package/hooks/hook-branchprotect/src/hook.ts +0 -88
  12. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  13. package/hooks/hook-checkbugs/src/cli.ts +0 -628
  14. package/hooks/hook-checkbugs/src/hook.ts +0 -335
  15. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  16. package/hooks/hook-checkdocs/src/cli.ts +0 -628
  17. package/hooks/hook-checkdocs/src/hook.ts +0 -310
  18. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  19. package/hooks/hook-checkfiles/src/cli.ts +0 -545
  20. package/hooks/hook-checkfiles/src/hook.ts +0 -321
  21. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  22. package/hooks/hook-checklint/src/cli-patch.ts +0 -32
  23. package/hooks/hook-checklint/src/cli.ts +0 -667
  24. package/hooks/hook-checklint/src/hook.ts +0 -473
  25. package/hooks/hook-checklint/tsconfig.json +0 -15
  26. package/hooks/hook-checkpoint/src/cli.ts +0 -191
  27. package/hooks/hook-checkpoint/src/hook.ts +0 -207
  28. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  29. package/hooks/hook-checksecurity/src/cli.ts +0 -601
  30. package/hooks/hook-checksecurity/src/hook.ts +0 -334
  31. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  32. package/hooks/hook-checktasks/src/cli.ts +0 -578
  33. package/hooks/hook-checktasks/src/hook.ts +0 -308
  34. package/hooks/hook-checktasks/tsconfig.json +0 -20
  35. package/hooks/hook-checktests/src/cli.ts +0 -627
  36. package/hooks/hook-checktests/src/hook.ts +0 -334
  37. package/hooks/hook-checktests/tsconfig.json +0 -15
  38. package/hooks/hook-contextrefresh/src/cli.ts +0 -152
  39. package/hooks/hook-contextrefresh/src/hook.ts +0 -148
  40. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  41. package/hooks/hook-gitguard/src/cli.ts +0 -159
  42. package/hooks/hook-gitguard/src/hook.ts +0 -129
  43. package/hooks/hook-gitguard/tsconfig.json +0 -25
  44. package/hooks/hook-packageage/src/cli.ts +0 -165
  45. package/hooks/hook-packageage/src/hook.ts +0 -177
  46. package/hooks/hook-packageage/tsconfig.json +0 -25
  47. package/hooks/hook-phonenotify/src/cli.ts +0 -196
  48. package/hooks/hook-phonenotify/src/hook.ts +0 -139
  49. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  50. package/hooks/hook-precompact/src/cli.ts +0 -168
  51. package/hooks/hook-precompact/src/hook.ts +0 -122
  52. package/hooks/hook-precompact/tsconfig.json +0 -25
  53. package/src/cli/components/App.tsx +0 -191
  54. package/src/cli/components/CategorySelect.tsx +0 -37
  55. package/src/cli/components/DataTable.tsx +0 -133
  56. package/src/cli/components/Header.tsx +0 -18
  57. package/src/cli/components/HookSelect.tsx +0 -29
  58. package/src/cli/components/InstallProgress.tsx +0 -105
  59. package/src/cli/components/SearchView.tsx +0 -86
  60. package/src/cli/index.tsx +0 -218
  61. package/src/index.ts +0 -31
  62. package/src/lib/installer.ts +0 -288
  63. package/src/lib/registry.ts +0 -205
  64. package/tsconfig.json +0 -17
@@ -1,321 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * Claude Code Hook: check-files
5
- *
6
- * Runs a headless Claude Code agent to review files and create tasks.
7
- * Uses service-implementation CLI to dispatch tasks.
8
- *
9
- * This hook runs ASYNC (non-blocking) on PostToolUse.
10
- *
11
- * Configuration:
12
- * - taskListId: task list for dispatching review tasks
13
- * - editThreshold: run review after this many edits (default: 3, range: 3-7)
14
- * - keywords: keywords that trigger the check (default: ["dev"])
15
- * - reviewPrompt: custom prompt for the headless agent
16
- * - enabled: enable/disable the hook
17
- */
18
-
19
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
20
- import { join } from "path";
21
- import { homedir } from "os";
22
- import { spawn } from "child_process";
23
-
24
- interface CheckFilesConfig {
25
- taskListId?: string;
26
- editThreshold?: number;
27
- keywords?: string[];
28
- reviewPrompt?: string;
29
- enabled?: boolean;
30
- }
31
-
32
- interface HookInput {
33
- session_id: string;
34
- transcript_path: string;
35
- cwd: string;
36
- tool_name: string;
37
- tool_input: Record<string, unknown>;
38
- tool_output?: string;
39
- }
40
-
41
- interface SessionState {
42
- editCount: number;
43
- editedFiles: string[];
44
- lastReviewRun: number;
45
- reviewInProgress: boolean;
46
- }
47
-
48
- const CONFIG_KEY = "checkFilesConfig";
49
- const STATE_DIR = join(homedir(), ".claude", "hook-state");
50
- const EDIT_TOOLS = ["Edit", "Write", "NotebookEdit"];
51
-
52
- /**
53
- * Sanitize ID to prevent path traversal and injection attacks
54
- */
55
- function sanitizeId(id: string): string {
56
- if (!id || typeof id !== 'string') return 'default';
57
- return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
58
- }
59
-
60
- /**
61
- * Sanitize file path for safe display in prompts
62
- */
63
- function sanitizePath(path: string): string {
64
- if (!path || typeof path !== 'string') return '';
65
- return path.replace(/[`$"'\\;&|<>(){}[\]!#*?~]/g, '_');
66
- }
67
-
68
- const DEFAULT_REVIEW_PROMPT = `You are a code reviewer. Review the following files that were recently edited and identify any issues:
69
-
70
- FILES TO REVIEW:
71
- {files}
72
-
73
- For each issue found, create a task using the service-implementation CLI:
74
- service-implementation task dispatch "{taskListId}" -s "REVIEW: [brief issue description]" -d "[detailed description with file:line reference]"
75
-
76
- Focus on:
77
- - Potential bugs or logic errors
78
- - Security vulnerabilities
79
- - Performance issues
80
- - Code style violations
81
- - Missing error handling
82
-
83
- If no issues are found, do not create any tasks.
84
- Only create tasks for real issues, not minor style preferences.
85
- Limit to max 5 most important issues.`;
86
-
87
- function readStdinJson(): HookInput | null {
88
- try {
89
- const stdin = readFileSync(0, "utf-8");
90
- return JSON.parse(stdin);
91
- } catch {
92
- return null;
93
- }
94
- }
95
-
96
- function readSettings(path: string): Record<string, unknown> {
97
- if (!existsSync(path)) return {};
98
- try {
99
- return JSON.parse(readFileSync(path, "utf-8"));
100
- } catch {
101
- return {};
102
- }
103
- }
104
-
105
- function getConfig(cwd: string): CheckFilesConfig {
106
- // Try project settings first
107
- const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
108
- if (projectSettings[CONFIG_KEY]) {
109
- return projectSettings[CONFIG_KEY] as CheckFilesConfig;
110
- }
111
-
112
- // Fall back to global settings
113
- const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
114
- if (globalSettings[CONFIG_KEY]) {
115
- return globalSettings[CONFIG_KEY] as CheckFilesConfig;
116
- }
117
-
118
- // Default config
119
- return {
120
- editThreshold: 3,
121
- keywords: ["dev"],
122
- enabled: true,
123
- };
124
- }
125
-
126
- function getStateFile(sessionId: string): string {
127
- mkdirSync(STATE_DIR, { recursive: true });
128
- const safeSessionId = sanitizeId(sessionId);
129
- return join(STATE_DIR, `checkfiles-${safeSessionId}.json`);
130
- }
131
-
132
- function getSessionState(sessionId: string): SessionState {
133
- const stateFile = getStateFile(sessionId);
134
- if (existsSync(stateFile)) {
135
- try {
136
- return JSON.parse(readFileSync(stateFile, "utf-8"));
137
- } catch {
138
- // Corrupted state, reset
139
- }
140
- }
141
- return { editCount: 0, editedFiles: [], lastReviewRun: 0, reviewInProgress: false };
142
- }
143
-
144
- function saveSessionState(sessionId: string, state: SessionState): void {
145
- const stateFile = getStateFile(sessionId);
146
- writeFileSync(stateFile, JSON.stringify(state, null, 2));
147
- }
148
-
149
- function getSessionName(transcriptPath: string): string | null {
150
- if (!existsSync(transcriptPath)) return null;
151
-
152
- try {
153
- const content = readFileSync(transcriptPath, "utf-8");
154
- let lastTitle: string | null = null;
155
- let searchStart = 0;
156
-
157
- while (true) {
158
- const titleIndex = content.indexOf('"custom-title"', searchStart);
159
- if (titleIndex === -1) break;
160
-
161
- const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
162
- const lineEnd = content.indexOf("\n", titleIndex);
163
- const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
164
-
165
- try {
166
- const entry = JSON.parse(line);
167
- if (entry.type === "custom-title" && entry.customTitle) {
168
- lastTitle = entry.customTitle;
169
- }
170
- } catch {
171
- // Skip malformed lines
172
- }
173
-
174
- searchStart = titleIndex + 1;
175
- }
176
-
177
- return lastTitle;
178
- } catch {
179
- return null;
180
- }
181
- }
182
-
183
- function getProjectTaskListId(cwd: string): string | null {
184
- const dirName = cwd.split("/").filter(Boolean).pop() || "";
185
- // Default to project-bugfixes or project-dev
186
- return `${dirName}-bugfixes`;
187
- }
188
-
189
- function runHeadlessReview(
190
- cwd: string,
191
- files: string[],
192
- taskListId: string,
193
- customPrompt?: string
194
- ): void {
195
- // Sanitize file paths to prevent prompt injection
196
- const filesFormatted = files.map((f) => `- ${sanitizePath(f)}`).join("\n");
197
- // Sanitize taskListId
198
- const safeTaskListId = sanitizeId(taskListId);
199
-
200
- const prompt = (customPrompt || DEFAULT_REVIEW_PROMPT)
201
- .replace("{files}", filesFormatted)
202
- .replace("{taskListId}", safeTaskListId);
203
-
204
- // Spawn headless Claude Code agent in background
205
- const child = spawn(
206
- "claude",
207
- [
208
- "-p",
209
- prompt,
210
- "--permission-mode",
211
- "acceptEdits",
212
- "--allowedTools",
213
- "Bash,Read",
214
- "--no-session-persistence",
215
- ],
216
- {
217
- cwd,
218
- detached: true,
219
- stdio: "ignore",
220
- }
221
- );
222
-
223
- // Detach from parent process
224
- child.unref();
225
-
226
- console.error(`[hook-checkfiles] Started headless review of ${files.length} files`);
227
- }
228
-
229
- function approve() {
230
- console.log(JSON.stringify({ decision: "approve" }));
231
- process.exit(0);
232
- }
233
-
234
- export function run() {
235
- const hookInput = readStdinJson();
236
- if (!hookInput) {
237
- approve();
238
- return;
239
- }
240
-
241
- const { session_id, cwd, tool_name, tool_input, transcript_path } = hookInput;
242
-
243
- // Only process edit tools
244
- if (!EDIT_TOOLS.includes(tool_name)) {
245
- approve();
246
- return;
247
- }
248
-
249
- const config = getConfig(cwd);
250
-
251
- // Check if hook is disabled
252
- if (config.enabled === false) {
253
- approve();
254
- return;
255
- }
256
-
257
- // Check keywords match
258
- const sessionName = transcript_path ? getSessionName(transcript_path) : null;
259
- const nameToCheck = sessionName || config.taskListId || "";
260
- const keywords = config.keywords || ["dev"];
261
-
262
- const matchesKeyword = keywords.some((keyword) =>
263
- nameToCheck.toLowerCase().includes(keyword.toLowerCase())
264
- );
265
-
266
- // If keywords are configured and we have a session name, check for match
267
- if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
268
- approve();
269
- return;
270
- }
271
-
272
- // Get edited file path
273
- const filePath = (tool_input.file_path || tool_input.notebook_path) as string | undefined;
274
- if (!filePath) {
275
- approve();
276
- return;
277
- }
278
-
279
- // Update session state
280
- const state = getSessionState(session_id);
281
-
282
- // Skip if review already in progress
283
- if (state.reviewInProgress) {
284
- approve();
285
- return;
286
- }
287
-
288
- state.editCount++;
289
-
290
- if (!state.editedFiles.includes(filePath)) {
291
- state.editedFiles.push(filePath);
292
- }
293
-
294
- const threshold = Math.min(7, Math.max(3, config.editThreshold || 3));
295
-
296
- // Check if we should run review
297
- if (state.editCount >= threshold) {
298
- const taskListId = config.taskListId || getProjectTaskListId(cwd) || "default-bugfixes";
299
-
300
- // Mark review in progress
301
- state.reviewInProgress = true;
302
- saveSessionState(session_id, state);
303
-
304
- // Run headless review (async, non-blocking)
305
- runHeadlessReview(cwd, state.editedFiles, taskListId, config.reviewPrompt);
306
-
307
- // Reset counter after starting review
308
- state.editCount = 0;
309
- state.editedFiles = [];
310
- state.lastReviewRun = Date.now();
311
- state.reviewInProgress = false;
312
- }
313
-
314
- saveSessionState(session_id, state);
315
- approve();
316
- }
317
-
318
- // Allow direct execution
319
- if (import.meta.main) {
320
- run();
321
- }
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "outDir": "./dist",
10
- "declaration": true,
11
- "declarationMap": true
12
- },
13
- "include": ["src/**/*"],
14
- "exclude": ["node_modules", "dist"]
15
- }
@@ -1,32 +0,0 @@
1
- interface InstallOptions {
2
- global?: boolean;
3
- taskListId?: string;
4
- keywords?: string[];
5
- editThreshold?: number;
6
- yes?: boolean;
7
- path?: string;
8
- }
9
-
10
- function parseInstallArgs(args: string[]): InstallOptions {
11
- const options: InstallOptions = {};
12
-
13
- for (let i = 0; i < args.length; i++) {
14
- const arg = args[i];
15
-
16
- if (arg === "--global" || arg === "-g") {
17
- options.global = true;
18
- } else if (arg === "--yes" || arg === "-y") {
19
- options.yes = true;
20
- } else if (arg === "--task-list-id" || arg === "-t") {
21
- options.taskListId = args[++i];
22
- } else if (arg === "--keywords" || arg === "-k") {
23
- options.keywords = args[++i]?.split(",").map(k => k.trim().toLowerCase()).filter(Boolean);
24
- } else if (arg === "--threshold" || arg === "-n") {
25
- options.editThreshold = parseInt(args[++i], 10);
26
- } else if (!arg.startsWith("-")) {
27
- options.path = arg;
28
- }
29
- }
30
-
31
- return options;
32
- }