@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,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
|
+
}
|