@hasna/hooks 0.0.1 → 0.0.2

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 (62) hide show
  1. package/dist/index.js +366 -0
  2. package/hooks/hook-agentmessages/bin/cli.ts +125 -0
  3. package/package.json +2 -2
  4. package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
  5. package/hooks/hook-agentmessages/src/install.ts +0 -126
  6. package/hooks/hook-agentmessages/src/session-start.ts +0 -255
  7. package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
  8. package/hooks/hook-branchprotect/src/cli.ts +0 -126
  9. package/hooks/hook-branchprotect/src/hook.ts +0 -88
  10. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  11. package/hooks/hook-checkbugs/src/cli.ts +0 -628
  12. package/hooks/hook-checkbugs/src/hook.ts +0 -335
  13. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  14. package/hooks/hook-checkdocs/src/cli.ts +0 -628
  15. package/hooks/hook-checkdocs/src/hook.ts +0 -310
  16. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  17. package/hooks/hook-checkfiles/src/cli.ts +0 -545
  18. package/hooks/hook-checkfiles/src/hook.ts +0 -321
  19. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  20. package/hooks/hook-checklint/src/cli-patch.ts +0 -32
  21. package/hooks/hook-checklint/src/cli.ts +0 -667
  22. package/hooks/hook-checklint/src/hook.ts +0 -473
  23. package/hooks/hook-checklint/tsconfig.json +0 -15
  24. package/hooks/hook-checkpoint/src/cli.ts +0 -191
  25. package/hooks/hook-checkpoint/src/hook.ts +0 -207
  26. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  27. package/hooks/hook-checksecurity/src/cli.ts +0 -601
  28. package/hooks/hook-checksecurity/src/hook.ts +0 -334
  29. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  30. package/hooks/hook-checktasks/src/cli.ts +0 -578
  31. package/hooks/hook-checktasks/src/hook.ts +0 -308
  32. package/hooks/hook-checktasks/tsconfig.json +0 -20
  33. package/hooks/hook-checktests/src/cli.ts +0 -627
  34. package/hooks/hook-checktests/src/hook.ts +0 -334
  35. package/hooks/hook-checktests/tsconfig.json +0 -15
  36. package/hooks/hook-contextrefresh/src/cli.ts +0 -152
  37. package/hooks/hook-contextrefresh/src/hook.ts +0 -148
  38. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  39. package/hooks/hook-gitguard/src/cli.ts +0 -159
  40. package/hooks/hook-gitguard/src/hook.ts +0 -129
  41. package/hooks/hook-gitguard/tsconfig.json +0 -25
  42. package/hooks/hook-packageage/src/cli.ts +0 -165
  43. package/hooks/hook-packageage/src/hook.ts +0 -177
  44. package/hooks/hook-packageage/tsconfig.json +0 -25
  45. package/hooks/hook-phonenotify/src/cli.ts +0 -196
  46. package/hooks/hook-phonenotify/src/hook.ts +0 -139
  47. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  48. package/hooks/hook-precompact/src/cli.ts +0 -168
  49. package/hooks/hook-precompact/src/hook.ts +0 -122
  50. package/hooks/hook-precompact/tsconfig.json +0 -25
  51. package/src/cli/components/App.tsx +0 -191
  52. package/src/cli/components/CategorySelect.tsx +0 -37
  53. package/src/cli/components/DataTable.tsx +0 -133
  54. package/src/cli/components/Header.tsx +0 -18
  55. package/src/cli/components/HookSelect.tsx +0 -29
  56. package/src/cli/components/InstallProgress.tsx +0 -105
  57. package/src/cli/components/SearchView.tsx +0 -86
  58. package/src/cli/index.tsx +0 -218
  59. package/src/index.ts +0 -31
  60. package/src/lib/installer.ts +0 -288
  61. package/src/lib/registry.ts +0 -205
  62. package/tsconfig.json +0 -17
@@ -1,473 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * Claude Code Hook: check-lint
5
- *
6
- * Runs linting after every N file edits and creates tasks for errors.
7
- *
8
- * Configuration priority:
9
- * 1. settings.json checkLintConfig (project or global)
10
- * 2. Environment variables (legacy)
11
- *
12
- * Config options:
13
- * - taskListId: task list for creating lint error tasks (default: auto-detect bugfixes list)
14
- * - lintCommand: lint command to run (default: auto-detect)
15
- * - editThreshold: run lint after this many edits (default: 3, range: 3-7)
16
- * - keywords: keywords that trigger the check (default: ["dev"])
17
- * - enabled: enable/disable the hook
18
- */
19
-
20
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
21
- import { join, dirname } from "path";
22
- import { homedir } from "os";
23
- import { execSync } from "child_process";
24
-
25
- interface CheckLintConfig {
26
- taskListId?: string;
27
- lintCommand?: string;
28
- editThreshold?: number;
29
- keywords?: string[];
30
- enabled?: boolean;
31
- createTasks?: boolean;
32
- }
33
-
34
- interface HookInput {
35
- session_id: string;
36
- transcript_path: string;
37
- cwd: string;
38
- tool_name: string;
39
- tool_input: Record<string, unknown>;
40
- tool_output?: string;
41
- }
42
-
43
- interface LintError {
44
- file: string;
45
- line: number;
46
- column: number;
47
- message: string;
48
- rule?: string;
49
- severity: "error" | "warning";
50
- }
51
-
52
- interface SessionState {
53
- editCount: number;
54
- editedFiles: string[];
55
- lastLintRun: number;
56
- }
57
-
58
- const CONFIG_KEY = "checkLintConfig";
59
- const STATE_DIR = join(homedir(), ".claude", "hook-state");
60
- const EDIT_TOOLS = ["Edit", "Write", "NotebookEdit"];
61
-
62
- // Allowed lint commands - whitelist approach for security
63
- const ALLOWED_LINT_COMMANDS = [
64
- "bun lint",
65
- "bun lint:check",
66
- "bun eslint",
67
- "bun biome check",
68
- "bunx @biomejs/biome check .",
69
- "bunx eslint .",
70
- "npm run lint",
71
- "npm run lint:check",
72
- "npx eslint .",
73
- "npx @biomejs/biome check .",
74
- ];
75
-
76
- /**
77
- * Sanitize ID to prevent path traversal and injection attacks
78
- */
79
- function sanitizeId(id: string): string {
80
- if (!id || typeof id !== 'string') return 'default';
81
- return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
82
- }
83
-
84
- /**
85
- * Validate lint command against whitelist to prevent command injection
86
- */
87
- function isValidLintCommand(cmd: string): boolean {
88
- if (!cmd || typeof cmd !== 'string') return false;
89
- return ALLOWED_LINT_COMMANDS.some(allowed =>
90
- cmd === allowed || cmd.startsWith(allowed + " ")
91
- );
92
- }
93
-
94
- function isValidRepoPattern(cwd: string): boolean {
95
- const dirName = cwd.split("/").filter(Boolean).pop() || "";
96
- // Match: hook-checklint, skill-installhook, iapp-mail, etc.
97
- return /^[a-z]+-[a-z0-9-]+$/i.test(dirName);
98
- }
99
-
100
- function readStdinJson(): HookInput | null {
101
- try {
102
- const stdin = readFileSync(0, "utf-8");
103
- return JSON.parse(stdin);
104
- } catch {
105
- return null;
106
- }
107
- }
108
-
109
- function readSettings(path: string): Record<string, unknown> {
110
- if (!existsSync(path)) return {};
111
- try {
112
- return JSON.parse(readFileSync(path, "utf-8"));
113
- } catch {
114
- return {};
115
- }
116
- }
117
-
118
- function getConfig(cwd: string): CheckLintConfig {
119
- // Try project settings first
120
- const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
121
- if (projectSettings[CONFIG_KEY]) {
122
- return projectSettings[CONFIG_KEY] as CheckLintConfig;
123
- }
124
-
125
- // Fall back to global settings
126
- const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
127
- if (globalSettings[CONFIG_KEY]) {
128
- return globalSettings[CONFIG_KEY] as CheckLintConfig;
129
- }
130
-
131
- // Default config
132
- return {
133
- editThreshold: 3,
134
- keywords: ["dev"],
135
- enabled: true,
136
- createTasks: true,
137
- };
138
- }
139
-
140
- function getStateFile(sessionId: string): string {
141
- mkdirSync(STATE_DIR, { recursive: true });
142
- const safeSessionId = sanitizeId(sessionId);
143
- return join(STATE_DIR, `checklint-${safeSessionId}.json`);
144
- }
145
-
146
- function getSessionState(sessionId: string): SessionState {
147
- const stateFile = getStateFile(sessionId);
148
- if (existsSync(stateFile)) {
149
- try {
150
- return JSON.parse(readFileSync(stateFile, "utf-8"));
151
- } catch {
152
- // Corrupted state, reset
153
- }
154
- }
155
- return { editCount: 0, editedFiles: [], lastLintRun: 0 };
156
- }
157
-
158
- function saveSessionState(sessionId: string, state: SessionState): void {
159
- const stateFile = getStateFile(sessionId);
160
- writeFileSync(stateFile, JSON.stringify(state, null, 2));
161
- }
162
-
163
- function detectLintCommand(cwd: string): string | null {
164
- const packageJsonPath = join(cwd, "package.json");
165
- if (existsSync(packageJsonPath)) {
166
- try {
167
- const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
168
- const scripts = pkg.scripts || {};
169
-
170
- // Check for common lint script names
171
- if (scripts.lint) return "bun lint";
172
- if (scripts["lint:check"]) return "bun lint:check";
173
- if (scripts.eslint) return "bun eslint";
174
- if (scripts.biome) return "bun biome check";
175
- } catch {
176
- // Ignore parse errors
177
- }
178
- }
179
-
180
- // Check for config files
181
- if (existsSync(join(cwd, "biome.json")) || existsSync(join(cwd, "biome.jsonc"))) {
182
- return "bunx @biomejs/biome check .";
183
- }
184
- if (existsSync(join(cwd, ".eslintrc.json")) || existsSync(join(cwd, ".eslintrc.js")) || existsSync(join(cwd, "eslint.config.js"))) {
185
- return "bunx eslint .";
186
- }
187
-
188
- return null;
189
- }
190
-
191
- function runLint(cwd: string, command: string): { success: boolean; output: string } {
192
- try {
193
- const output = execSync(command, {
194
- cwd,
195
- encoding: "utf-8",
196
- stdio: ["pipe", "pipe", "pipe"],
197
- timeout: 60000, // 60s timeout
198
- });
199
- return { success: true, output };
200
- } catch (error: unknown) {
201
- const execError = error as { stdout?: string; stderr?: string };
202
- const output = (execError.stdout || "") + (execError.stderr || "");
203
- return { success: false, output };
204
- }
205
- }
206
-
207
- function parseLintOutput(output: string): LintError[] {
208
- const errors: LintError[] = [];
209
- const lines = output.split("\n");
210
-
211
- for (const line of lines) {
212
- // ESLint format: /path/file.ts:10:5: error Message [rule-name]
213
- // Biome format: path/file.ts:10:5 lint/rule ERROR message
214
- // Common format: file:line:col: message
215
-
216
- // Try ESLint/common format
217
- const eslintMatch = line.match(/^(.+?):(\d+):(\d+):\s*(error|warning)\s+(.+?)(?:\s+\[(.+?)\])?$/i);
218
- if (eslintMatch) {
219
- errors.push({
220
- file: eslintMatch[1],
221
- line: parseInt(eslintMatch[2], 10),
222
- column: parseInt(eslintMatch[3], 10),
223
- severity: eslintMatch[4].toLowerCase() as "error" | "warning",
224
- message: eslintMatch[5].trim(),
225
- rule: eslintMatch[6],
226
- });
227
- continue;
228
- }
229
-
230
- // Try Biome format
231
- const biomeMatch = line.match(/^(.+?):(\d+):(\d+)\s+(\w+\/\w+)\s+(ERROR|WARNING|WARN)\s+(.+)$/i);
232
- if (biomeMatch) {
233
- errors.push({
234
- file: biomeMatch[1],
235
- line: parseInt(biomeMatch[2], 10),
236
- column: parseInt(biomeMatch[3], 10),
237
- rule: biomeMatch[4],
238
- severity: biomeMatch[5].toUpperCase().startsWith("ERR") ? "error" : "warning",
239
- message: biomeMatch[6].trim(),
240
- });
241
- continue;
242
- }
243
-
244
- // Generic file:line:col format
245
- const genericMatch = line.match(/^(.+?):(\d+):(\d+):\s*(.+)$/);
246
- if (genericMatch && !genericMatch[1].startsWith(" ")) {
247
- errors.push({
248
- file: genericMatch[1],
249
- line: parseInt(genericMatch[2], 10),
250
- column: parseInt(genericMatch[3], 10),
251
- severity: line.toLowerCase().includes("error") ? "error" : "warning",
252
- message: genericMatch[4].trim(),
253
- });
254
- }
255
- }
256
-
257
- return errors;
258
- }
259
-
260
- function getProjectTaskList(cwd: string): string | null {
261
- const tasksDir = join(homedir(), ".claude", "tasks");
262
- if (!existsSync(tasksDir)) return null;
263
-
264
- const dirName = cwd.split("/").filter(Boolean).pop() || "";
265
-
266
- try {
267
- const lists = readdirSync(tasksDir, { withFileTypes: true })
268
- .filter((d) => d.isDirectory())
269
- .map((d) => d.name);
270
-
271
- // Look for a bugfixes list for this project
272
- const bugfixList = lists.find((list) => {
273
- const listLower = list.toLowerCase();
274
- const dirLower = dirName.toLowerCase();
275
- return listLower.startsWith(dirLower) && listLower.includes("bugfix");
276
- });
277
-
278
- if (bugfixList) return bugfixList;
279
-
280
- // Fall back to dev list
281
- const devList = lists.find((list) => {
282
- const listLower = list.toLowerCase();
283
- const dirLower = dirName.toLowerCase();
284
- return listLower.startsWith(dirLower) && listLower.includes("dev");
285
- });
286
-
287
- return devList || null;
288
- } catch {
289
- return null;
290
- }
291
- }
292
-
293
- function createTask(taskListId: string, error: LintError): void {
294
- const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
295
- mkdirSync(tasksDir, { recursive: true });
296
-
297
- const taskId = `lint-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
298
- const severity = error.severity === "error" ? "MEDIUM" : "LOW";
299
- const ruleInfo = error.rule ? ` (${error.rule})` : "";
300
-
301
- const task = {
302
- id: taskId,
303
- subject: `BUG: ${severity} - Fix lint ${error.severity} in ${error.file}:${error.line}`,
304
- description: `Fix lint ${error.severity} at ${error.file}:${error.line}:${error.column}\n\n**Error:** ${error.message}${ruleInfo}\n\n**File:** ${error.file}\n**Line:** ${error.line}\n**Column:** ${error.column}${error.rule ? `\n**Rule:** ${error.rule}` : ""}\n\n**Acceptance criteria:**\n- Lint error is fixed\n- No new lint errors introduced`,
305
- status: "pending",
306
- createdAt: new Date().toISOString(),
307
- metadata: {
308
- source: "hook-checklint",
309
- file: error.file,
310
- line: error.line,
311
- column: error.column,
312
- rule: error.rule,
313
- severity: error.severity,
314
- },
315
- };
316
-
317
- const taskFile = join(tasksDir, `${taskId}.json`);
318
- writeFileSync(taskFile, JSON.stringify(task, null, 2));
319
- }
320
-
321
- function getSessionName(transcriptPath: string): string | null {
322
- if (!existsSync(transcriptPath)) return null;
323
-
324
- try {
325
- const content = readFileSync(transcriptPath, "utf-8");
326
- let lastTitle: string | null = null;
327
- let searchStart = 0;
328
-
329
- while (true) {
330
- const titleIndex = content.indexOf('"custom-title"', searchStart);
331
- if (titleIndex === -1) break;
332
-
333
- const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
334
- const lineEnd = content.indexOf("\n", titleIndex);
335
- const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
336
-
337
- try {
338
- const entry = JSON.parse(line);
339
- if (entry.type === "custom-title" && entry.customTitle) {
340
- lastTitle = entry.customTitle;
341
- }
342
- } catch {
343
- // Skip malformed lines
344
- }
345
-
346
- searchStart = titleIndex + 1;
347
- }
348
-
349
- return lastTitle;
350
- } catch {
351
- return null;
352
- }
353
- }
354
-
355
- function approve() {
356
- console.log(JSON.stringify({ decision: "approve" }));
357
- process.exit(0);
358
- }
359
-
360
- export function run() {
361
- const hookInput = readStdinJson();
362
- if (!hookInput) {
363
- approve();
364
- return;
365
- }
366
-
367
- const { session_id, cwd, tool_name, tool_input, transcript_path } = hookInput;
368
-
369
- // Only process edit tools
370
- if (!EDIT_TOOLS.includes(tool_name)) {
371
- approve();
372
- return;
373
- }
374
-
375
- // Check repo pattern - only run for [prefix]-[name] folders
376
- if (!isValidRepoPattern(cwd)) {
377
- approve();
378
- return;
379
- }
380
-
381
- const config = getConfig(cwd);
382
-
383
- // Check if hook is disabled
384
- if (config.enabled === false) {
385
- approve();
386
- return;
387
- }
388
-
389
- // Check keywords match
390
- const sessionName = transcript_path ? getSessionName(transcript_path) : null;
391
- const nameToCheck = sessionName || config.taskListId || "";
392
- const keywords = config.keywords || ["dev"];
393
-
394
- const matchesKeyword = keywords.some((keyword) =>
395
- nameToCheck.toLowerCase().includes(keyword.toLowerCase())
396
- );
397
-
398
- // If keywords are configured and we have a session name, check for match
399
- if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
400
- approve();
401
- return;
402
- }
403
-
404
- // Get edited file path
405
- const filePath = (tool_input.file_path || tool_input.notebook_path) as string | undefined;
406
- if (!filePath) {
407
- approve();
408
- return;
409
- }
410
-
411
- // Update session state
412
- const state = getSessionState(session_id);
413
- state.editCount++;
414
-
415
- if (!state.editedFiles.includes(filePath)) {
416
- state.editedFiles.push(filePath);
417
- }
418
-
419
- const threshold = Math.min(7, Math.max(3, config.editThreshold || 3));
420
-
421
- // Check if we should run lint
422
- if (state.editCount >= threshold) {
423
- // Detect or use configured lint command
424
- const lintCommand = config.lintCommand || detectLintCommand(cwd);
425
-
426
- // Validate lint command against whitelist to prevent command injection
427
- if (lintCommand && isValidLintCommand(lintCommand)) {
428
- const { success, output } = runLint(cwd, lintCommand);
429
-
430
- if (!success) {
431
- const errors = parseLintOutput(output);
432
-
433
- // Filter to errors in files we edited (optional - could check all)
434
- const relevantErrors = errors.filter((e) =>
435
- state.editedFiles.some((f) => f.endsWith(e.file) || e.file.endsWith(f.split("/").pop() || ""))
436
- );
437
-
438
- // Create tasks for lint errors if enabled
439
- if (config.createTasks !== false && relevantErrors.length > 0) {
440
- const taskListId = config.taskListId || getProjectTaskList(cwd);
441
-
442
- if (taskListId) {
443
- // Limit to first 5 errors to avoid task spam
444
- const errorsToReport = relevantErrors.slice(0, 5);
445
- for (const error of errorsToReport) {
446
- createTask(taskListId, error);
447
- }
448
-
449
- // Log summary
450
- console.error(
451
- `[hook-checklint] Created ${errorsToReport.length} task(s) for lint errors in "${taskListId}"`
452
- );
453
- if (relevantErrors.length > 5) {
454
- console.error(`[hook-checklint] (${relevantErrors.length - 5} more errors not reported)`);
455
- }
456
- }
457
- }
458
- }
459
- }
460
-
461
- // Reset counter after lint run
462
- state.editCount = 0;
463
- state.lastLintRun = Date.now();
464
- }
465
-
466
- saveSessionState(session_id, state);
467
- approve();
468
- }
469
-
470
- // Allow direct execution
471
- if (import.meta.main) {
472
- run();
473
- }
@@ -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,191 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * CLI for hook-checkpoint
5
- */
6
-
7
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
- import { execSync } from "child_process";
9
- import { join } from "path";
10
- import { homedir } from "os";
11
-
12
- const HOOK_NAME = "hook-checkpoint";
13
- const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
14
- const CHECKPOINT_DIR = ".claude-checkpoints";
15
-
16
- interface ClaudeSettings {
17
- hooks?: {
18
- PreToolUse?: Array<{
19
- matcher: string;
20
- hooks: Array<{ type: "command"; command: string }>;
21
- }>;
22
- };
23
- [key: string]: unknown;
24
- }
25
-
26
- function readSettings(): ClaudeSettings {
27
- try {
28
- if (existsSync(SETTINGS_PATH)) {
29
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
30
- }
31
- } catch {}
32
- return {};
33
- }
34
-
35
- function writeSettings(settings: ClaudeSettings): void {
36
- const dir = join(homedir(), ".claude");
37
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
38
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
39
- }
40
-
41
- function install(): void {
42
- const settings = readSettings();
43
- if (!settings.hooks) settings.hooks = {};
44
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
45
-
46
- const existing = settings.hooks.PreToolUse.find((h) =>
47
- h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
48
- );
49
-
50
- if (existing) {
51
- console.log(`${HOOK_NAME} is already installed`);
52
- return;
53
- }
54
-
55
- settings.hooks.PreToolUse.push({
56
- matcher: "Write|Edit|NotebookEdit",
57
- hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
58
- });
59
-
60
- writeSettings(settings);
61
- console.log(`${HOOK_NAME} installed successfully`);
62
- console.log("Hook will create shadow git snapshots before Write/Edit/NotebookEdit");
63
- }
64
-
65
- function uninstall(): void {
66
- const settings = readSettings();
67
- if (!settings.hooks?.PreToolUse) {
68
- console.log(`${HOOK_NAME} is not installed`);
69
- return;
70
- }
71
-
72
- const before = settings.hooks.PreToolUse.length;
73
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
74
- (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
75
- );
76
-
77
- if (before === settings.hooks.PreToolUse.length) {
78
- console.log(`${HOOK_NAME} is not installed`);
79
- return;
80
- }
81
-
82
- writeSettings(settings);
83
- console.log(`${HOOK_NAME} uninstalled successfully`);
84
- }
85
-
86
- function status(): void {
87
- const settings = readSettings();
88
- const installed = settings.hooks?.PreToolUse?.some((h) =>
89
- h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
90
- );
91
- console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
92
- }
93
-
94
- function list(): void {
95
- const checkpointDir = join(process.cwd(), CHECKPOINT_DIR);
96
- if (!existsSync(checkpointDir)) {
97
- console.log("No checkpoints found in current directory");
98
- return;
99
- }
100
-
101
- try {
102
- const log = execSync("git log --oneline -20", {
103
- cwd: checkpointDir,
104
- encoding: "utf-8",
105
- });
106
- console.log("Recent checkpoints:");
107
- console.log(log);
108
- } catch {
109
- console.log("No checkpoints found");
110
- }
111
- }
112
-
113
- function restore(ref: string): void {
114
- const checkpointDir = join(process.cwd(), CHECKPOINT_DIR);
115
- if (!existsSync(checkpointDir)) {
116
- console.log("No checkpoints found in current directory");
117
- return;
118
- }
119
-
120
- try {
121
- const files = execSync(`git show ${ref} --name-only --pretty=format:""`, {
122
- cwd: checkpointDir,
123
- encoding: "utf-8",
124
- }).trim().split("\n").filter((f) => f.startsWith("files/"));
125
-
126
- for (const file of files) {
127
- const relativePath = file.replace("files/", "");
128
- try {
129
- const content = execSync(`git show ${ref}:${file}`, {
130
- cwd: checkpointDir,
131
- });
132
- const targetPath = join(process.cwd(), relativePath);
133
- writeFileSync(targetPath, content);
134
- console.log(`Restored: ${relativePath}`);
135
- } catch {
136
- console.error(`Failed to restore: ${relativePath}`);
137
- }
138
- }
139
- } catch (error) {
140
- console.error(`Failed to restore from ${ref}:`, error);
141
- }
142
- }
143
-
144
- function help(): void {
145
- console.log(`
146
- ${HOOK_NAME} - Shadow git snapshots for Claude Code file modifications
147
-
148
- Usage: ${HOOK_NAME} <command>
149
-
150
- Commands:
151
- install Install hook to Claude Code settings
152
- uninstall Remove hook from Claude Code settings
153
- status Check if hook is installed
154
- list Show recent checkpoints in current directory
155
- restore <ref> Restore files from a checkpoint (git ref)
156
- help Show this help message
157
-
158
- How it works:
159
- Before any Write/Edit/NotebookEdit, the hook copies the original file
160
- into a shadow git repo (.claude-checkpoints/) and commits it. This gives
161
- you a full history of every file before Claude modified it.
162
- `);
163
- }
164
-
165
- const command = process.argv[2];
166
-
167
- switch (command) {
168
- case "install": install(); break;
169
- case "uninstall": uninstall(); break;
170
- case "status": status(); break;
171
- case "list": list(); break;
172
- case "restore":
173
- if (process.argv[3]) {
174
- restore(process.argv[3]);
175
- } else {
176
- console.error("Usage: hook-checkpoint restore <git-ref>");
177
- process.exit(1);
178
- }
179
- break;
180
- case "help":
181
- case "--help":
182
- case "-h": help(); break;
183
- default:
184
- if (!command) {
185
- import("./hook.ts").then((m) => m.run());
186
- } else {
187
- console.error(`Unknown command: ${command}`);
188
- help();
189
- process.exit(1);
190
- }
191
- }