@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
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: checkpoint
5
+ *
6
+ * PreToolUse hook that creates shadow git snapshots before any file
7
+ * Write/Edit operations. This provides a safety net with easy rollback
8
+ * capability without cluttering the main project's git history.
9
+ *
10
+ * Shadow repo is stored in .claude-checkpoints/ (gitignored).
11
+ */
12
+
13
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from "fs";
14
+ import { execSync } from "child_process";
15
+ import { join, resolve } from "path";
16
+
17
+ interface HookInput {
18
+ session_id: string;
19
+ cwd: string;
20
+ tool_name: string;
21
+ tool_input: Record<string, unknown>;
22
+ }
23
+
24
+ interface HookOutput {
25
+ decision?: "approve" | "block";
26
+ reason?: string;
27
+ }
28
+
29
+ const CHECKPOINT_DIR = ".claude-checkpoints";
30
+ const TOOLS_TO_CHECKPOINT = ["Write", "Edit", "NotebookEdit"];
31
+
32
+ /**
33
+ * Read JSON from stdin
34
+ */
35
+ function readStdinJson(): HookInput | null {
36
+ try {
37
+ const input = readFileSync(0, "utf-8").trim();
38
+ if (!input) return null;
39
+ return JSON.parse(input);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Initialize shadow git repo for checkpoints
47
+ */
48
+ function initShadowRepo(cwd: string): string {
49
+ const shadowPath = join(cwd, CHECKPOINT_DIR);
50
+
51
+ if (!existsSync(shadowPath)) {
52
+ mkdirSync(shadowPath, { recursive: true });
53
+ }
54
+
55
+ const gitDir = join(shadowPath, ".git");
56
+ if (!existsSync(gitDir)) {
57
+ execSync("git init", { cwd: shadowPath, stdio: "pipe" });
58
+ execSync('git config user.email "hook-checkpoint@claude.local"', {
59
+ cwd: shadowPath,
60
+ stdio: "pipe",
61
+ });
62
+ execSync('git config user.name "hook-checkpoint"', {
63
+ cwd: shadowPath,
64
+ stdio: "pipe",
65
+ });
66
+
67
+ // Create initial commit
68
+ writeFileSync(join(shadowPath, ".checkpoint-init"), new Date().toISOString());
69
+ execSync("git add -A && git commit -m 'init: checkpoint repo'", {
70
+ cwd: shadowPath,
71
+ stdio: "pipe",
72
+ });
73
+ }
74
+
75
+ // Ensure .claude-checkpoints is gitignored in main project
76
+ const mainGitignore = join(cwd, ".gitignore");
77
+ if (existsSync(mainGitignore)) {
78
+ const content = readFileSync(mainGitignore, "utf-8");
79
+ if (!content.includes(CHECKPOINT_DIR)) {
80
+ appendFileSync(mainGitignore, `\n${CHECKPOINT_DIR}/\n`);
81
+ }
82
+ }
83
+
84
+ return shadowPath;
85
+ }
86
+
87
+ /**
88
+ * Copy the target file into the shadow repo and commit
89
+ */
90
+ function createCheckpoint(
91
+ cwd: string,
92
+ shadowPath: string,
93
+ toolName: string,
94
+ filePath: string,
95
+ sessionId: string
96
+ ): void {
97
+ const absFilePath = resolve(cwd, filePath);
98
+
99
+ if (!existsSync(absFilePath)) {
100
+ // File doesn't exist yet (new file being created) — log but no snapshot needed
101
+ const logEntry = `[${new Date().toISOString()}] ${toolName} creating new file: ${filePath}\n`;
102
+ appendFileSync(join(shadowPath, "checkpoint.log"), logEntry);
103
+ return;
104
+ }
105
+
106
+ // Copy file to shadow repo preserving relative path
107
+ const relativePath = filePath.startsWith("/")
108
+ ? filePath.replace(cwd, "").replace(/^\//, "")
109
+ : filePath;
110
+ const shadowFilePath = join(shadowPath, "files", relativePath);
111
+ const shadowFileDir = join(shadowFilePath, "..");
112
+
113
+ mkdirSync(shadowFileDir, { recursive: true });
114
+
115
+ const content = readFileSync(absFilePath);
116
+ writeFileSync(shadowFilePath, content);
117
+
118
+ // Create metadata
119
+ const metadata = {
120
+ tool: toolName,
121
+ file: filePath,
122
+ session: sessionId,
123
+ timestamp: new Date().toISOString(),
124
+ };
125
+ writeFileSync(
126
+ join(shadowPath, "last-checkpoint.json"),
127
+ JSON.stringify(metadata, null, 2)
128
+ );
129
+
130
+ // Commit to shadow repo
131
+ try {
132
+ execSync("git add -A", { cwd: shadowPath, stdio: "pipe" });
133
+ const msg = `checkpoint: ${toolName} ${relativePath}`;
134
+ execSync(`git commit -m "${msg}" --allow-empty`, {
135
+ cwd: shadowPath,
136
+ stdio: "pipe",
137
+ });
138
+ } catch {
139
+ // Commit may fail if nothing changed — that's fine
140
+ }
141
+
142
+ // Log
143
+ const logEntry = `[${new Date().toISOString()}] ${toolName} → ${relativePath} (session: ${sessionId})\n`;
144
+ appendFileSync(join(shadowPath, "checkpoint.log"), logEntry);
145
+ }
146
+
147
+ /**
148
+ * Extract file path from tool input
149
+ */
150
+ function getFilePath(toolName: string, toolInput: Record<string, unknown>): string | null {
151
+ switch (toolName) {
152
+ case "Write":
153
+ case "Edit":
154
+ return (toolInput.file_path as string) || null;
155
+ case "NotebookEdit":
156
+ return (toolInput.notebook_path as string) || null;
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Output hook response
164
+ */
165
+ function respond(output: HookOutput): void {
166
+ console.log(JSON.stringify(output));
167
+ }
168
+
169
+ /**
170
+ * Main hook execution
171
+ */
172
+ export function run(): void {
173
+ const input = readStdinJson();
174
+
175
+ if (!input) {
176
+ respond({ decision: "approve" });
177
+ return;
178
+ }
179
+
180
+ // Only checkpoint file-modifying tools
181
+ if (!TOOLS_TO_CHECKPOINT.includes(input.tool_name)) {
182
+ respond({ decision: "approve" });
183
+ return;
184
+ }
185
+
186
+ const filePath = getFilePath(input.tool_name, input.tool_input);
187
+ if (!filePath) {
188
+ respond({ decision: "approve" });
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const shadowPath = initShadowRepo(input.cwd);
194
+ createCheckpoint(input.cwd, shadowPath, input.tool_name, filePath, input.session_id);
195
+ } catch (error) {
196
+ // Never block operations due to checkpoint failures
197
+ const errMsg = error instanceof Error ? error.message : String(error);
198
+ console.error(`[hook-checkpoint] Warning: checkpoint failed: ${errMsg}`);
199
+ }
200
+
201
+ // Always approve — checkpointing is non-blocking
202
+ respond({ decision: "approve" });
203
+ }
204
+
205
+ if (import.meta.main) {
206
+ run();
207
+ }