@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.
Files changed (110) hide show
  1. package/.npmrc.example +2 -0
  2. package/AGENTS.md +54 -0
  3. package/CLAUDE.md +70 -0
  4. package/CONTRIBUTING.md +45 -0
  5. package/README.md +232 -0
  6. package/bin/index.js +5171 -0
  7. package/hooks/hook-agentmessages/CLAUDE.md +79 -0
  8. package/hooks/hook-agentmessages/LICENSE +21 -0
  9. package/hooks/hook-agentmessages/README.md +107 -0
  10. package/hooks/hook-agentmessages/package.json +31 -0
  11. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  12. package/hooks/hook-agentmessages/src/install.ts +126 -0
  13. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  14. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  15. package/hooks/hook-branchprotect/CLAUDE.md +23 -0
  16. package/hooks/hook-branchprotect/README.md +25 -0
  17. package/hooks/hook-branchprotect/package.json +42 -0
  18. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  19. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  20. package/hooks/hook-branchprotect/tsconfig.json +25 -0
  21. package/hooks/hook-checkbugs/LICENSE +21 -0
  22. package/hooks/hook-checkbugs/README.md +140 -0
  23. package/hooks/hook-checkbugs/package.json +58 -0
  24. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  25. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  26. package/hooks/hook-checkbugs/tsconfig.json +15 -0
  27. package/hooks/hook-checkdocs/README.md +137 -0
  28. package/hooks/hook-checkdocs/package.json +57 -0
  29. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  30. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  31. package/hooks/hook-checkdocs/tsconfig.json +15 -0
  32. package/hooks/hook-checkfiles/LICENSE +21 -0
  33. package/hooks/hook-checkfiles/README.md +141 -0
  34. package/hooks/hook-checkfiles/package.json +56 -0
  35. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  36. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  37. package/hooks/hook-checkfiles/tsconfig.json +15 -0
  38. package/hooks/hook-checklint/LICENSE +21 -0
  39. package/hooks/hook-checklint/README.md +147 -0
  40. package/hooks/hook-checklint/package.json +57 -0
  41. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  42. package/hooks/hook-checklint/src/cli.ts +667 -0
  43. package/hooks/hook-checklint/src/hook.ts +473 -0
  44. package/hooks/hook-checklint/tsconfig.json +15 -0
  45. package/hooks/hook-checkpoint/CLAUDE.md +23 -0
  46. package/hooks/hook-checkpoint/README.md +37 -0
  47. package/hooks/hook-checkpoint/package.json +58 -0
  48. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  49. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  50. package/hooks/hook-checkpoint/tsconfig.json +25 -0
  51. package/hooks/hook-checksecurity/LICENSE +21 -0
  52. package/hooks/hook-checksecurity/README.md +158 -0
  53. package/hooks/hook-checksecurity/package.json +57 -0
  54. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  55. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  56. package/hooks/hook-checksecurity/tsconfig.json +15 -0
  57. package/hooks/hook-checktasks/README.md +144 -0
  58. package/hooks/hook-checktasks/package.json +55 -0
  59. package/hooks/hook-checktasks/src/cli.ts +578 -0
  60. package/hooks/hook-checktasks/src/hook.ts +308 -0
  61. package/hooks/hook-checktasks/tsconfig.json +20 -0
  62. package/hooks/hook-checktests/LICENSE +21 -0
  63. package/hooks/hook-checktests/README.md +137 -0
  64. package/hooks/hook-checktests/package.json +57 -0
  65. package/hooks/hook-checktests/src/cli.ts +627 -0
  66. package/hooks/hook-checktests/src/hook.ts +334 -0
  67. package/hooks/hook-checktests/tsconfig.json +15 -0
  68. package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
  69. package/hooks/hook-contextrefresh/README.md +42 -0
  70. package/hooks/hook-contextrefresh/package.json +42 -0
  71. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  72. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  73. package/hooks/hook-contextrefresh/tsconfig.json +25 -0
  74. package/hooks/hook-gitguard/CLAUDE.md +22 -0
  75. package/hooks/hook-gitguard/README.md +30 -0
  76. package/hooks/hook-gitguard/package.json +57 -0
  77. package/hooks/hook-gitguard/src/cli.ts +159 -0
  78. package/hooks/hook-gitguard/src/hook.ts +129 -0
  79. package/hooks/hook-gitguard/tsconfig.json +25 -0
  80. package/hooks/hook-packageage/CLAUDE.md +23 -0
  81. package/hooks/hook-packageage/README.md +33 -0
  82. package/hooks/hook-packageage/package.json +42 -0
  83. package/hooks/hook-packageage/src/cli.ts +165 -0
  84. package/hooks/hook-packageage/src/hook.ts +177 -0
  85. package/hooks/hook-packageage/tsconfig.json +25 -0
  86. package/hooks/hook-phonenotify/CLAUDE.md +25 -0
  87. package/hooks/hook-phonenotify/README.md +44 -0
  88. package/hooks/hook-phonenotify/package.json +42 -0
  89. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  90. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  91. package/hooks/hook-phonenotify/tsconfig.json +25 -0
  92. package/hooks/hook-precompact/CLAUDE.md +23 -0
  93. package/hooks/hook-precompact/README.md +36 -0
  94. package/hooks/hook-precompact/package.json +42 -0
  95. package/hooks/hook-precompact/src/cli.ts +168 -0
  96. package/hooks/hook-precompact/src/hook.ts +122 -0
  97. package/hooks/hook-precompact/tsconfig.json +25 -0
  98. package/package.json +61 -0
  99. package/src/cli/components/App.tsx +191 -0
  100. package/src/cli/components/CategorySelect.tsx +37 -0
  101. package/src/cli/components/DataTable.tsx +133 -0
  102. package/src/cli/components/Header.tsx +18 -0
  103. package/src/cli/components/HookSelect.tsx +29 -0
  104. package/src/cli/components/InstallProgress.tsx +105 -0
  105. package/src/cli/components/SearchView.tsx +86 -0
  106. package/src/cli/index.tsx +218 -0
  107. package/src/index.ts +31 -0
  108. package/src/lib/installer.ts +288 -0
  109. package/src/lib/registry.ts +205 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: check-tests
5
+ *
6
+ * Runs a headless Claude Code agent to check for missing tests.
7
+ * Uses service-implementation CLI to dispatch tasks.
8
+ *
9
+ * This hook runs ASYNC (non-blocking) on PostToolUse.
10
+ * Only runs for repos matching [prefix]-[name] pattern.
11
+ *
12
+ * Configuration:
13
+ * - taskListId: task list for dispatching test tasks
14
+ * - editThreshold: run check after this many edits (default: 3, range: 3-7)
15
+ * - keywords: keywords that trigger the check (default: ["dev"])
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 CheckTestsConfig {
25
+ taskListId?: string;
26
+ editThreshold?: number;
27
+ keywords?: string[];
28
+ enabled?: boolean;
29
+ }
30
+
31
+ interface HookInput {
32
+ session_id: string;
33
+ transcript_path: string;
34
+ cwd: string;
35
+ tool_name: string;
36
+ tool_input: Record<string, unknown>;
37
+ tool_output?: string;
38
+ }
39
+
40
+ interface SessionState {
41
+ editCount: number;
42
+ editedFiles: string[];
43
+ lastCheckRun: number;
44
+ checkInProgress: boolean;
45
+ }
46
+
47
+ const CONFIG_KEY = "checkTestsConfig";
48
+ const STATE_DIR = join(homedir(), ".claude", "hook-state");
49
+ const EDIT_TOOLS = ["Edit", "Write", "NotebookEdit"];
50
+
51
+ /**
52
+ * Sanitize ID to prevent path traversal and injection attacks
53
+ */
54
+ function sanitizeId(id: string): string {
55
+ if (!id || typeof id !== 'string') return 'default';
56
+ return id.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 100) || 'default';
57
+ }
58
+
59
+ /**
60
+ * Sanitize file path for safe display in prompts
61
+ */
62
+ function sanitizePath(path: string): string {
63
+ if (!path || typeof path !== 'string') return '';
64
+ return path.replace(/[`$"'\\;&|<>(){}[\]!#*?~]/g, '_');
65
+ }
66
+
67
+ const TESTS_PROMPT = `You are a test coverage reviewer. Review the following files that were recently edited and identify missing or incomplete tests:
68
+
69
+ FILES TO REVIEW:
70
+ {files}
71
+
72
+ For each test coverage issue found, create a task using the service-implementation CLI:
73
+ service-implementation task dispatch "{taskListId}" -s "TEST: [brief description]" -d "[detailed description of what tests need to be added]"
74
+
75
+ Focus on:
76
+ - Missing unit tests for new functions/methods
77
+ - Missing integration tests for new features
78
+ - Missing edge case tests
79
+ - Missing error handling tests
80
+ - Untested code paths
81
+ - Missing mock/stub implementations
82
+ - Missing test fixtures or setup
83
+ - Missing API endpoint tests
84
+ - Missing validation tests
85
+
86
+ If no test coverage issues are found, do not create any tasks.
87
+ Only create tasks for meaningful test gaps, not trivial ones.
88
+ Limit to max 5 most important test tasks.`;
89
+
90
+ function isValidRepoPattern(cwd: string): boolean {
91
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
92
+ // Match: hook-checklint, skill-installhook, iapp-mail, etc.
93
+ return /^[a-z]+-[a-z0-9-]+$/i.test(dirName);
94
+ }
95
+
96
+ function readStdinJson(): HookInput | null {
97
+ try {
98
+ const stdin = readFileSync(0, "utf-8");
99
+ return JSON.parse(stdin);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function readSettings(path: string): Record<string, unknown> {
106
+ if (!existsSync(path)) return {};
107
+ try {
108
+ return JSON.parse(readFileSync(path, "utf-8"));
109
+ } catch {
110
+ return {};
111
+ }
112
+ }
113
+
114
+ function getConfig(cwd: string): CheckTestsConfig {
115
+ // Try project settings first
116
+ const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
117
+ if (projectSettings[CONFIG_KEY]) {
118
+ return projectSettings[CONFIG_KEY] as CheckTestsConfig;
119
+ }
120
+
121
+ // Fall back to global settings
122
+ const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
123
+ if (globalSettings[CONFIG_KEY]) {
124
+ return globalSettings[CONFIG_KEY] as CheckTestsConfig;
125
+ }
126
+
127
+ // Default config
128
+ return {
129
+ editThreshold: 3,
130
+ keywords: ["dev"],
131
+ enabled: true,
132
+ };
133
+ }
134
+
135
+ function getStateFile(sessionId: string): string {
136
+ mkdirSync(STATE_DIR, { recursive: true });
137
+ const safeSessionId = sanitizeId(sessionId);
138
+ return join(STATE_DIR, `checktests-${safeSessionId}.json`);
139
+ }
140
+
141
+ function getSessionState(sessionId: string): SessionState {
142
+ const stateFile = getStateFile(sessionId);
143
+ if (existsSync(stateFile)) {
144
+ try {
145
+ return JSON.parse(readFileSync(stateFile, "utf-8"));
146
+ } catch {
147
+ // Corrupted state, reset
148
+ }
149
+ }
150
+ return { editCount: 0, editedFiles: [], lastCheckRun: 0, checkInProgress: false };
151
+ }
152
+
153
+ function saveSessionState(sessionId: string, state: SessionState): void {
154
+ const stateFile = getStateFile(sessionId);
155
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
156
+ }
157
+
158
+ function getSessionName(transcriptPath: string): string | null {
159
+ if (!existsSync(transcriptPath)) return null;
160
+
161
+ try {
162
+ const content = readFileSync(transcriptPath, "utf-8");
163
+ let lastTitle: string | null = null;
164
+ let searchStart = 0;
165
+
166
+ while (true) {
167
+ const titleIndex = content.indexOf('"custom-title"', searchStart);
168
+ if (titleIndex === -1) break;
169
+
170
+ const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
171
+ const lineEnd = content.indexOf("\n", titleIndex);
172
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
173
+
174
+ try {
175
+ const entry = JSON.parse(line);
176
+ if (entry.type === "custom-title" && entry.customTitle) {
177
+ lastTitle = entry.customTitle;
178
+ }
179
+ } catch {
180
+ // Skip malformed lines
181
+ }
182
+
183
+ searchStart = titleIndex + 1;
184
+ }
185
+
186
+ return lastTitle;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
192
+ function getProjectTaskListId(cwd: string): string | null {
193
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
194
+ return `${dirName}-qa`;
195
+ }
196
+
197
+ function runHeadlessTestsCheck(
198
+ cwd: string,
199
+ files: string[],
200
+ taskListId: string
201
+ ): void {
202
+ // Sanitize file paths to prevent prompt injection
203
+ const filesFormatted = files.map((f) => `- ${sanitizePath(f)}`).join("\n");
204
+ // Sanitize taskListId
205
+ const safeTaskListId = sanitizeId(taskListId);
206
+
207
+ const prompt = TESTS_PROMPT
208
+ .replace("{files}", filesFormatted)
209
+ .replace("{taskListId}", safeTaskListId);
210
+
211
+ // Spawn headless Claude Code agent in background
212
+ const child = spawn(
213
+ "claude",
214
+ [
215
+ "-p",
216
+ prompt,
217
+ "--permission-mode",
218
+ "acceptEdits",
219
+ "--allowedTools",
220
+ "Bash,Read",
221
+ "--no-session-persistence",
222
+ ],
223
+ {
224
+ cwd,
225
+ detached: true,
226
+ stdio: "ignore",
227
+ }
228
+ );
229
+
230
+ // Detach from parent process
231
+ child.unref();
232
+
233
+ console.error(`[hook-checktests] Started tests check for ${files.length} files`);
234
+ }
235
+
236
+ function approve() {
237
+ console.log(JSON.stringify({ decision: "approve" }));
238
+ process.exit(0);
239
+ }
240
+
241
+ export function run() {
242
+ const hookInput = readStdinJson();
243
+ if (!hookInput) {
244
+ approve();
245
+ return;
246
+ }
247
+
248
+ const { session_id, cwd, tool_name, tool_input, transcript_path } = hookInput;
249
+
250
+ // Only process edit tools
251
+ if (!EDIT_TOOLS.includes(tool_name)) {
252
+ approve();
253
+ return;
254
+ }
255
+
256
+ // Check repo pattern - only run for [prefix]-[name] folders
257
+ if (!isValidRepoPattern(cwd)) {
258
+ approve();
259
+ return;
260
+ }
261
+
262
+ const config = getConfig(cwd);
263
+
264
+ // Check if hook is disabled
265
+ if (config.enabled === false) {
266
+ approve();
267
+ return;
268
+ }
269
+
270
+ // Check keywords match
271
+ const sessionName = transcript_path ? getSessionName(transcript_path) : null;
272
+ const nameToCheck = sessionName || config.taskListId || "";
273
+ const keywords = config.keywords || ["dev"];
274
+
275
+ const matchesKeyword = keywords.some((keyword) =>
276
+ nameToCheck.toLowerCase().includes(keyword.toLowerCase())
277
+ );
278
+
279
+ // If keywords are configured and we have a session name, check for match
280
+ if (keywords.length > 0 && nameToCheck && !matchesKeyword) {
281
+ approve();
282
+ return;
283
+ }
284
+
285
+ // Get edited file path
286
+ const filePath = (tool_input.file_path || tool_input.notebook_path) as string | undefined;
287
+ if (!filePath) {
288
+ approve();
289
+ return;
290
+ }
291
+
292
+ // Update session state
293
+ const state = getSessionState(session_id);
294
+
295
+ // Skip if check already in progress
296
+ if (state.checkInProgress) {
297
+ approve();
298
+ return;
299
+ }
300
+
301
+ state.editCount++;
302
+
303
+ if (!state.editedFiles.includes(filePath)) {
304
+ state.editedFiles.push(filePath);
305
+ }
306
+
307
+ const threshold = Math.min(7, Math.max(3, config.editThreshold || 3));
308
+
309
+ // Check if we should run tests check
310
+ if (state.editCount >= threshold) {
311
+ const taskListId = config.taskListId || getProjectTaskListId(cwd) || "default-qa";
312
+
313
+ // Mark check in progress
314
+ state.checkInProgress = true;
315
+ saveSessionState(session_id, state);
316
+
317
+ // Run headless tests check (async, non-blocking)
318
+ runHeadlessTestsCheck(cwd, state.editedFiles, taskListId);
319
+
320
+ // Reset counter after starting check
321
+ state.editCount = 0;
322
+ state.editedFiles = [];
323
+ state.lastCheckRun = Date.now();
324
+ state.checkInProgress = false;
325
+ }
326
+
327
+ saveSessionState(session_id, state);
328
+ approve();
329
+ }
330
+
331
+ // Allow direct execution
332
+ if (import.meta.main) {
333
+ run();
334
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,23 @@
1
+ # CLAUDE.md
2
+
3
+ ## hook-contextrefresh
4
+
5
+ A UserPromptSubmit hook that re-injects important context every N prompts.
6
+
7
+ ### Key Files
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `src/hook.ts` | Main hook logic — tracks prompt count, injects context |
12
+ | `src/cli.ts` | CLI — install/uninstall/status |
13
+
14
+ ### Hook Events
15
+
16
+ - **UserPromptSubmit** — fires before Claude processes each user prompt
17
+
18
+ ### Behavior
19
+
20
+ - Tracks prompt count per session in temp files
21
+ - Every N prompts, reads `.claude-context` from project root
22
+ - Prepends context to user's prompt as a refresh
23
+ - Configurable interval and context file path
@@ -0,0 +1,42 @@
1
+ # hook-contextrefresh
2
+
3
+ Claude Code hook that re-injects important context every N prompts to prevent context decay.
4
+
5
+ ## Overview
6
+
7
+ In long Claude Code sessions, important instructions can drift out of the active context window. This hook automatically re-injects content from a `.claude-context` file every N user prompts.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun install -g @hasnaxyz/hook-contextrefresh
13
+ hook-contextrefresh install 10 # inject every 10 prompts
14
+ ```
15
+
16
+ Then create `.claude-context` in your project root with the context you want refreshed.
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ hook-contextrefresh install [N] # Install with interval (default: 10)
22
+ hook-contextrefresh uninstall # Remove hook
23
+ hook-contextrefresh status # Show config
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ In `~/.claude/settings.json`:
29
+
30
+ ```json
31
+ {
32
+ "contextRefreshConfig": {
33
+ "enabled": true,
34
+ "interval": 10,
35
+ "contextFile": ".claude-context"
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-contextrefresh",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that re-injects important context every N prompts to prevent context decay",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-contextrefresh": "./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": ["dist", "README.md"],
20
+ "scripts": {
21
+ "build": "bun build ./src/cli.ts ./src/hook.ts --outdir ./dist --target node",
22
+ "prepublishOnly": "bun run build",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": ["claude-code", "claude", "hook", "context", "refresh", "memory", "cli"],
26
+ "author": "Hasna",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/hasnaxyz/hook-contextrefresh.git"
31
+ },
32
+ "publishConfig": {
33
+ "access": "restricted",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "engines": { "node": ">=18", "bun": ">=1.0" },
37
+ "devDependencies": {
38
+ "@types/bun": "^1.3.8",
39
+ "@types/node": "^20",
40
+ "typescript": "^5.0.0"
41
+ }
42
+ }
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * CLI for hook-contextrefresh
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ const HOOK_NAME = "hook-contextrefresh";
12
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
13
+
14
+ interface ClaudeSettings {
15
+ hooks?: {
16
+ UserPromptSubmit?: Array<{
17
+ matcher?: string;
18
+ hooks: Array<{ type: "command"; command: string }>;
19
+ }>;
20
+ };
21
+ contextRefreshConfig?: {
22
+ enabled?: boolean;
23
+ interval?: number;
24
+ contextFile?: string;
25
+ };
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ function readSettings(): ClaudeSettings {
30
+ try {
31
+ if (existsSync(SETTINGS_PATH)) {
32
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
33
+ }
34
+ } catch {}
35
+ return {};
36
+ }
37
+
38
+ function writeSettings(settings: ClaudeSettings): void {
39
+ const dir = join(homedir(), ".claude");
40
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
41
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
42
+ }
43
+
44
+ function install(interval?: string): void {
45
+ const settings = readSettings();
46
+ if (!settings.hooks) settings.hooks = {};
47
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
48
+
49
+ const existing = settings.hooks.UserPromptSubmit.find((h) =>
50
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
51
+ );
52
+
53
+ if (existing) {
54
+ console.log(`${HOOK_NAME} is already installed`);
55
+ return;
56
+ }
57
+
58
+ settings.hooks.UserPromptSubmit.push({
59
+ hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
60
+ });
61
+
62
+ if (!settings.contextRefreshConfig) {
63
+ settings.contextRefreshConfig = {
64
+ enabled: true,
65
+ interval: interval ? parseInt(interval, 10) : 10,
66
+ };
67
+ }
68
+
69
+ writeSettings(settings);
70
+ console.log(`${HOOK_NAME} installed successfully`);
71
+ console.log(`Interval: every ${settings.contextRefreshConfig.interval} prompts`);
72
+ console.log(`\nCreate a .claude-context file in your project root with the context to inject.`);
73
+ }
74
+
75
+ function uninstall(): void {
76
+ const settings = readSettings();
77
+ if (!settings.hooks?.UserPromptSubmit) {
78
+ console.log(`${HOOK_NAME} is not installed`);
79
+ return;
80
+ }
81
+
82
+ const before = settings.hooks.UserPromptSubmit.length;
83
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
84
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
85
+ );
86
+
87
+ if (before === settings.hooks.UserPromptSubmit.length) {
88
+ console.log(`${HOOK_NAME} is not installed`);
89
+ return;
90
+ }
91
+
92
+ writeSettings(settings);
93
+ console.log(`${HOOK_NAME} uninstalled successfully`);
94
+ }
95
+
96
+ function status(): void {
97
+ const settings = readSettings();
98
+ const installed = settings.hooks?.UserPromptSubmit?.some((h) =>
99
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
100
+ );
101
+ console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
102
+
103
+ if (settings.contextRefreshConfig) {
104
+ console.log(`\nConfig:`);
105
+ console.log(` Enabled: ${settings.contextRefreshConfig.enabled !== false}`);
106
+ console.log(` Interval: every ${settings.contextRefreshConfig.interval || 10} prompts`);
107
+ console.log(` Context file: ${settings.contextRefreshConfig.contextFile || ".claude-context"}`);
108
+ }
109
+
110
+ // Check if context file exists
111
+ const contextFile = join(process.cwd(), ".claude-context");
112
+ console.log(`\nContext file: ${existsSync(contextFile) ? "found" : "not found"} (${contextFile})`);
113
+ }
114
+
115
+ function help(): void {
116
+ console.log(`
117
+ ${HOOK_NAME} - Re-inject context every N prompts
118
+
119
+ Usage: ${HOOK_NAME} <command>
120
+
121
+ Commands:
122
+ install [N] Install hook (optional: set interval, default 10)
123
+ uninstall Remove hook from Claude Code settings
124
+ status Check if hook is installed and show config
125
+ help Show this help message
126
+
127
+ Setup:
128
+ 1. Run: ${HOOK_NAME} install
129
+ 2. Create .claude-context in your project root
130
+ 3. Add important context/rules to that file
131
+ 4. Context is re-injected every N prompts automatically
132
+ `);
133
+ }
134
+
135
+ const command = process.argv[2];
136
+
137
+ switch (command) {
138
+ case "install": install(process.argv[3]); break;
139
+ case "uninstall": uninstall(); break;
140
+ case "status": status(); break;
141
+ case "help":
142
+ case "--help":
143
+ case "-h": help(); break;
144
+ default:
145
+ if (!command) {
146
+ import("./hook.ts").then((m) => m.run());
147
+ } else {
148
+ console.error(`Unknown command: ${command}`);
149
+ help();
150
+ process.exit(1);
151
+ }
152
+ }