@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.
- package/dist/index.js +366 -0
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/package.json +2 -2
- package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
- package/hooks/hook-agentmessages/src/install.ts +0 -126
- package/hooks/hook-agentmessages/src/session-start.ts +0 -255
- package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
- package/hooks/hook-branchprotect/src/cli.ts +0 -126
- package/hooks/hook-branchprotect/src/hook.ts +0 -88
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/src/cli.ts +0 -628
- package/hooks/hook-checkbugs/src/hook.ts +0 -335
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/src/cli.ts +0 -628
- package/hooks/hook-checkdocs/src/hook.ts +0 -310
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/src/cli.ts +0 -545
- package/hooks/hook-checkfiles/src/hook.ts +0 -321
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/src/cli-patch.ts +0 -32
- package/hooks/hook-checklint/src/cli.ts +0 -667
- package/hooks/hook-checklint/src/hook.ts +0 -473
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/src/cli.ts +0 -191
- package/hooks/hook-checkpoint/src/hook.ts +0 -207
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/src/cli.ts +0 -601
- package/hooks/hook-checksecurity/src/hook.ts +0 -334
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/src/cli.ts +0 -578
- package/hooks/hook-checktasks/src/hook.ts +0 -308
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/src/cli.ts +0 -627
- package/hooks/hook-checktests/src/hook.ts +0 -334
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-contextrefresh/src/cli.ts +0 -152
- package/hooks/hook-contextrefresh/src/hook.ts +0 -148
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-gitguard/src/cli.ts +0 -159
- package/hooks/hook-gitguard/src/hook.ts +0 -129
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-packageage/src/cli.ts +0 -165
- package/hooks/hook-packageage/src/hook.ts +0 -177
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/src/cli.ts +0 -196
- package/hooks/hook-phonenotify/src/hook.ts +0 -139
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/src/cli.ts +0 -168
- package/hooks/hook-precompact/src/hook.ts +0 -122
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/src/cli/components/App.tsx +0 -191
- package/src/cli/components/CategorySelect.tsx +0 -37
- package/src/cli/components/DataTable.tsx +0 -133
- package/src/cli/components/Header.tsx +0 -18
- package/src/cli/components/HookSelect.tsx +0 -29
- package/src/cli/components/InstallProgress.tsx +0 -105
- package/src/cli/components/SearchView.tsx +0 -86
- package/src/cli/index.tsx +0 -218
- package/src/index.ts +0 -31
- package/src/lib/installer.ts +0 -288
- package/src/lib/registry.ts +0 -205
- package/tsconfig.json +0 -17
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Claude Code Hook: check-tasks
|
|
5
|
-
*
|
|
6
|
-
* Prevents Claude from stopping when there are pending/in-progress tasks.
|
|
7
|
-
*
|
|
8
|
-
* Configuration priority:
|
|
9
|
-
* 1. settings.json checkTasksConfig (project or global)
|
|
10
|
-
* 2. Environment variables (legacy)
|
|
11
|
-
*
|
|
12
|
-
* Config options:
|
|
13
|
-
* - taskListId: specific list to check, or undefined = check all lists
|
|
14
|
-
* - keywords: keywords that trigger the check (default: ["dev"])
|
|
15
|
-
* - enabled: enable/disable the hook
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
19
|
-
import { join } from "path";
|
|
20
|
-
import { homedir } from "os";
|
|
21
|
-
|
|
22
|
-
interface Task {
|
|
23
|
-
id: string;
|
|
24
|
-
subject: string;
|
|
25
|
-
status: "pending" | "in_progress" | "completed";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface CheckTasksConfig {
|
|
29
|
-
taskListId?: string;
|
|
30
|
-
keywords?: string[];
|
|
31
|
-
enabled?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface HookInput {
|
|
35
|
-
session_id: string;
|
|
36
|
-
transcript_path: string;
|
|
37
|
-
cwd: string;
|
|
38
|
-
permission_mode: string;
|
|
39
|
-
hook_event_name: string;
|
|
40
|
-
stop_hook_active: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const CONFIG_KEY = "checkTasksConfig";
|
|
44
|
-
|
|
45
|
-
function readStdinJson(): HookInput | null {
|
|
46
|
-
try {
|
|
47
|
-
const stdin = readFileSync(0, "utf-8");
|
|
48
|
-
return JSON.parse(stdin);
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function readSettings(path: string): Record<string, unknown> {
|
|
55
|
-
if (!existsSync(path)) return {};
|
|
56
|
-
try {
|
|
57
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
58
|
-
} catch {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getConfig(cwd: string): CheckTasksConfig {
|
|
64
|
-
// Try project settings first
|
|
65
|
-
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
66
|
-
if (projectSettings[CONFIG_KEY]) {
|
|
67
|
-
return projectSettings[CONFIG_KEY] as CheckTasksConfig;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Fall back to global settings
|
|
71
|
-
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
72
|
-
if (globalSettings[CONFIG_KEY]) {
|
|
73
|
-
return globalSettings[CONFIG_KEY] as CheckTasksConfig;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Legacy: use environment variables
|
|
77
|
-
return {
|
|
78
|
-
taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
|
|
79
|
-
keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
|
|
80
|
-
enabled: process.env.CHECK_TASKS_DISABLED !== "1",
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getSessionName(transcriptPath: string): string | null {
|
|
85
|
-
if (!existsSync(transcriptPath)) return null;
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const content = readFileSync(transcriptPath, "utf-8");
|
|
89
|
-
let lastTitle: string | null = null;
|
|
90
|
-
let searchStart = 0;
|
|
91
|
-
|
|
92
|
-
while (true) {
|
|
93
|
-
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
94
|
-
if (titleIndex === -1) break;
|
|
95
|
-
|
|
96
|
-
const lineStart = content.lastIndexOf("\n", titleIndex) + 1;
|
|
97
|
-
const lineEnd = content.indexOf("\n", titleIndex);
|
|
98
|
-
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const entry = JSON.parse(line);
|
|
102
|
-
if (entry.type === "custom-title" && entry.customTitle) {
|
|
103
|
-
lastTitle = entry.customTitle;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// Skip malformed lines
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
searchStart = titleIndex + 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return lastTitle;
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function getAllTaskLists(): string[] {
|
|
119
|
-
const tasksDir = join(homedir(), ".claude", "tasks");
|
|
120
|
-
if (!existsSync(tasksDir)) return [];
|
|
121
|
-
try {
|
|
122
|
-
return readdirSync(tasksDir, { withFileTypes: true })
|
|
123
|
-
.filter((d) => d.isDirectory())
|
|
124
|
-
.map((d) => d.name);
|
|
125
|
-
} catch {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getProjectTaskLists(cwd: string): string[] {
|
|
131
|
-
const allLists = getAllTaskLists();
|
|
132
|
-
|
|
133
|
-
// Get the directory name as project identifier
|
|
134
|
-
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
135
|
-
|
|
136
|
-
// Filter lists that match the project name
|
|
137
|
-
const projectLists = allLists.filter((list) => {
|
|
138
|
-
const listLower = list.toLowerCase();
|
|
139
|
-
const dirLower = dirName.toLowerCase();
|
|
140
|
-
|
|
141
|
-
// Exact prefix match (e.g., "connect-x" matches "connect-x-dev")
|
|
142
|
-
if (listLower.startsWith(dirLower + "-")) return true;
|
|
143
|
-
|
|
144
|
-
// Exact match
|
|
145
|
-
if (listLower === dirLower) return true;
|
|
146
|
-
|
|
147
|
-
return false;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return projectLists;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function getTasksFromList(listId: string): Task[] {
|
|
154
|
-
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
155
|
-
if (!existsSync(tasksDir)) return [];
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
159
|
-
return taskFiles.map((file) => {
|
|
160
|
-
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
161
|
-
return JSON.parse(content) as Task;
|
|
162
|
-
});
|
|
163
|
-
} catch {
|
|
164
|
-
return [];
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function approve() {
|
|
169
|
-
console.log(JSON.stringify({ decision: "approve" }));
|
|
170
|
-
process.exit(0);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function block(reason: string) {
|
|
174
|
-
console.log(JSON.stringify({ decision: "block", reason }));
|
|
175
|
-
process.exit(0);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function run() {
|
|
179
|
-
const hookInput = readStdinJson();
|
|
180
|
-
const cwd = hookInput?.cwd || process.cwd();
|
|
181
|
-
|
|
182
|
-
const config = getConfig(cwd);
|
|
183
|
-
|
|
184
|
-
// Check if hook is disabled
|
|
185
|
-
if (config.enabled === false) {
|
|
186
|
-
approve();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Get session name from transcript
|
|
190
|
-
let sessionName: string | null = null;
|
|
191
|
-
if (hookInput?.transcript_path) {
|
|
192
|
-
sessionName = getSessionName(hookInput.transcript_path);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Determine what to check for keywords
|
|
196
|
-
const nameToCheck = sessionName || config.taskListId || "";
|
|
197
|
-
const keywords = config.keywords || ["dev"];
|
|
198
|
-
|
|
199
|
-
// Only block stop for sessions matching configured keywords
|
|
200
|
-
const matchesKeyword = keywords.some((keyword) =>
|
|
201
|
-
nameToCheck.toLowerCase().includes(keyword.toLowerCase())
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
205
|
-
// Not a matching session, allow stop
|
|
206
|
-
approve();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Determine which task lists to check
|
|
210
|
-
let listsToCheck: string[] = [];
|
|
211
|
-
|
|
212
|
-
if (config.taskListId) {
|
|
213
|
-
// Specific list configured
|
|
214
|
-
listsToCheck = [config.taskListId];
|
|
215
|
-
} else {
|
|
216
|
-
// Get lists that belong to this project/folder
|
|
217
|
-
const projectLists = getProjectTaskLists(cwd);
|
|
218
|
-
|
|
219
|
-
if (projectLists.length > 0) {
|
|
220
|
-
// Filter by keywords if specified
|
|
221
|
-
if (keywords.length > 0) {
|
|
222
|
-
listsToCheck = projectLists.filter((list) =>
|
|
223
|
-
keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase()))
|
|
224
|
-
);
|
|
225
|
-
} else {
|
|
226
|
-
listsToCheck = projectLists;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// If no project-specific lists found, don't check any (don't fall back to all lists)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (listsToCheck.length === 0) {
|
|
233
|
-
// No matching task lists, allow stop
|
|
234
|
-
approve();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Collect tasks from all matching lists
|
|
238
|
-
let allPending: Task[] = [];
|
|
239
|
-
let allInProgress: Task[] = [];
|
|
240
|
-
let allCompleted: Task[] = [];
|
|
241
|
-
let activeListId: string | null = null;
|
|
242
|
-
|
|
243
|
-
for (const listId of listsToCheck) {
|
|
244
|
-
const tasks = getTasksFromList(listId);
|
|
245
|
-
const pending = tasks.filter((t) => t.status === "pending");
|
|
246
|
-
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
247
|
-
const completed = tasks.filter((t) => t.status === "completed");
|
|
248
|
-
|
|
249
|
-
if (pending.length > 0 || inProgress.length > 0) {
|
|
250
|
-
activeListId = listId;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
allPending.push(...pending);
|
|
254
|
-
allInProgress.push(...inProgress);
|
|
255
|
-
allCompleted.push(...completed);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const remainingCount = allPending.length + allInProgress.length;
|
|
259
|
-
|
|
260
|
-
if (remainingCount > 0) {
|
|
261
|
-
const nextTasks = allPending
|
|
262
|
-
.slice(0, 3)
|
|
263
|
-
.map((t) => `- ${t.subject}`)
|
|
264
|
-
.join("\n");
|
|
265
|
-
|
|
266
|
-
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
267
|
-
|
|
268
|
-
const prompt = `
|
|
269
|
-
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
270
|
-
|
|
271
|
-
⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
|
|
272
|
-
|
|
273
|
-
You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
|
|
274
|
-
|
|
275
|
-
Next pending tasks:
|
|
276
|
-
${nextTasks}
|
|
277
|
-
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
278
|
-
|
|
279
|
-
MANDATORY INSTRUCTIONS (follow these NOW):
|
|
280
|
-
1. Use TaskList to see all tasks
|
|
281
|
-
2. Use TaskGet to read the FIRST pending task's full description
|
|
282
|
-
3. Use TaskUpdate to mark it as in_progress BEFORE starting work
|
|
283
|
-
4. Complete the task (write code, run commands, etc.)
|
|
284
|
-
5. Use TaskUpdate to mark it as completed AFTER finishing
|
|
285
|
-
6. IMMEDIATELY move to the next task - DO NOT STOP
|
|
286
|
-
|
|
287
|
-
CRITICAL RULES:
|
|
288
|
-
- NEVER ask "would you like me to..." - just DO IT
|
|
289
|
-
- NEVER ask for confirmation - just WORK
|
|
290
|
-
- NEVER stop to explain what you'll do - just DO IT
|
|
291
|
-
- If a task is unclear, make reasonable assumptions and proceed
|
|
292
|
-
- If you encounter an error, fix it and continue
|
|
293
|
-
- Keep working until remainingCount = 0
|
|
294
|
-
|
|
295
|
-
START WORKING NOW. Use TaskList tool in your next response.
|
|
296
|
-
`.trim();
|
|
297
|
-
|
|
298
|
-
block(prompt);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// All tasks completed, allow stop
|
|
302
|
-
approve();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Allow direct execution
|
|
306
|
-
if (import.meta.main) {
|
|
307
|
-
run();
|
|
308
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"noEmit": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"declarationMap": true,
|
|
14
|
-
"outDir": "./dist",
|
|
15
|
-
"rootDir": "./src",
|
|
16
|
-
"types": ["bun-types", "node"]
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*.ts"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|