@hasnaxyz/hook-checktasks 1.0.0 → 1.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/README.md +89 -97
- package/dist/cli.js +458 -121
- package/dist/hook.js +133 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,150 +1,142 @@
|
|
|
1
1
|
# @hasnaxyz/hook-checktasks
|
|
2
2
|
|
|
3
|
-
A Claude Code hook that prevents Claude from stopping when there are pending tasks
|
|
3
|
+
A Claude Code hook that prevents Claude from stopping when there are pending tasks.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This hook intercepts Claude's "Stop" event and:
|
|
8
8
|
|
|
9
|
-
1. Checks
|
|
10
|
-
2.
|
|
11
|
-
3. If
|
|
12
|
-
4. Only allows stopping when all tasks are completed
|
|
13
|
-
|
|
14
|
-
This ensures Claude doesn't abandon work mid-session.
|
|
9
|
+
1. Checks configured task lists for pending/in-progress tasks
|
|
10
|
+
2. If tasks remain → **blocks the stop** and prompts Claude to continue
|
|
11
|
+
3. If all complete → allows stop
|
|
15
12
|
|
|
16
13
|
## Installation
|
|
17
14
|
|
|
18
|
-
###
|
|
19
|
-
|
|
20
|
-
- [Bun](https://bun.sh/) runtime installed
|
|
21
|
-
- Access to `@hasnaxyz` npm organization
|
|
22
|
-
|
|
23
|
-
### Quick Install
|
|
15
|
+
### 1. Install the CLI globally
|
|
24
16
|
|
|
25
17
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Install for current project only
|
|
30
|
-
bunx @hasnaxyz/hook-checktasks install
|
|
18
|
+
bun add -g @hasnaxyz/hook-checktasks
|
|
19
|
+
# or
|
|
20
|
+
npm install -g @hasnaxyz/hook-checktasks
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
###
|
|
23
|
+
### 2. Install the hook
|
|
34
24
|
|
|
35
25
|
```bash
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
# Auto-detect (git repo → project, else → prompt)
|
|
27
|
+
hook-checktasks install
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
# Remove from global settings
|
|
43
|
-
bunx @hasnaxyz/hook-checktasks uninstall --global
|
|
29
|
+
# Install globally
|
|
30
|
+
hook-checktasks install --global
|
|
44
31
|
|
|
45
|
-
#
|
|
46
|
-
|
|
32
|
+
# Install to specific path
|
|
33
|
+
hook-checktasks install /path/to/project
|
|
47
34
|
```
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
The installer will prompt you to configure:
|
|
37
|
+
- **Task list ID**: specific list or leave empty for all lists
|
|
38
|
+
- **Keywords**: session/list name keywords to trigger the check (default: "dev")
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|----------|-------------|---------|
|
|
55
|
-
| `CLAUDE_CODE_TASK_LIST_ID` | Task list to monitor (required for task checking) | - |
|
|
56
|
-
| `CHECK_TASKS_KEYWORDS` | Comma-separated keywords that trigger the check | `dev` |
|
|
57
|
-
| `CHECK_TASKS_DISABLED` | Set to `1` to disable the hook | - |
|
|
40
|
+
## Configuration
|
|
58
41
|
|
|
59
|
-
###
|
|
42
|
+
### Update configuration
|
|
60
43
|
|
|
61
44
|
```bash
|
|
62
|
-
#
|
|
63
|
-
|
|
45
|
+
hook-checktasks config # Current project
|
|
46
|
+
hook-checktasks config --global # Global settings
|
|
47
|
+
```
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
CHECK_TASKS_KEYWORDS=dev,sprint CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
|
|
49
|
+
### Check status
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
```bash
|
|
52
|
+
hook-checktasks status
|
|
70
53
|
```
|
|
71
54
|
|
|
72
|
-
|
|
55
|
+
Shows:
|
|
56
|
+
- Where hook is installed (global/project)
|
|
57
|
+
- Current configuration
|
|
58
|
+
- Available task lists
|
|
59
|
+
|
|
60
|
+
### Uninstall
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- If tasks remain: Block stop with instructions to continue
|
|
80
|
-
- If all complete: Allow stop
|
|
62
|
+
```bash
|
|
63
|
+
hook-checktasks uninstall # Current project
|
|
64
|
+
hook-checktasks uninstall --global # Global
|
|
65
|
+
hook-checktasks uninstall /path # Specific path
|
|
66
|
+
```
|
|
81
67
|
|
|
82
|
-
##
|
|
68
|
+
## How Configuration Works
|
|
83
69
|
|
|
84
|
-
|
|
70
|
+
Configuration is stored in `.claude/settings.json`:
|
|
85
71
|
|
|
86
72
|
```json
|
|
87
73
|
{
|
|
88
74
|
"hooks": {
|
|
89
|
-
"Stop": [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"timeout": 120
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
]
|
|
75
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "bunx @hasnaxyz/hook-checktasks run" }] }]
|
|
76
|
+
},
|
|
77
|
+
"checkTasksConfig": {
|
|
78
|
+
"taskListId": "myproject-dev",
|
|
79
|
+
"keywords": ["dev"],
|
|
80
|
+
"enabled": true
|
|
100
81
|
}
|
|
101
82
|
}
|
|
102
83
|
```
|
|
103
84
|
|
|
104
|
-
|
|
85
|
+
### Config Options
|
|
105
86
|
|
|
106
|
-
|
|
87
|
+
| Option | Description | Default |
|
|
88
|
+
|--------|-------------|---------|
|
|
89
|
+
| `taskListId` | Specific task list to check, or `undefined` for all lists | `undefined` (all) |
|
|
90
|
+
| `keywords` | Keywords to match in session/list names | `["dev"]` |
|
|
91
|
+
| `enabled` | Enable/disable the hook | `true` |
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
"id": "task-001",
|
|
111
|
-
"subject": "Implement user authentication",
|
|
112
|
-
"status": "pending"
|
|
113
|
-
}
|
|
114
|
-
```
|
|
93
|
+
### Priority
|
|
115
94
|
|
|
116
|
-
|
|
95
|
+
1. Project settings (`.claude/settings.json`)
|
|
96
|
+
2. Global settings (`~/.claude/settings.json`)
|
|
97
|
+
3. Environment variables (legacy)
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```
|
|
121
|
-
bunx @hasnaxyz/hook-checktasks <command> [options]
|
|
99
|
+
### Legacy Environment Variables
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
install [--global] Install the hook to Claude Code settings
|
|
125
|
-
uninstall [--global] Remove the hook from Claude Code settings
|
|
126
|
-
run Execute the hook (called by Claude Code automatically)
|
|
127
|
-
status Show current hook configuration
|
|
101
|
+
Still supported for backwards compatibility:
|
|
128
102
|
|
|
129
|
-
|
|
130
|
-
|
|
103
|
+
```bash
|
|
104
|
+
CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
|
|
105
|
+
CHECK_TASKS_KEYWORDS=dev,sprint claude
|
|
106
|
+
CHECK_TASKS_DISABLED=1 claude
|
|
131
107
|
```
|
|
132
108
|
|
|
133
|
-
##
|
|
109
|
+
## CLI Commands
|
|
134
110
|
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
111
|
+
```
|
|
112
|
+
hook-checktasks install [path] Install the hook
|
|
113
|
+
hook-checktasks config [path] Update configuration
|
|
114
|
+
hook-checktasks uninstall [path] Remove the hook
|
|
115
|
+
hook-checktasks status Show hook status
|
|
116
|
+
hook-checktasks run Execute hook (called by Claude Code)
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
|
|
118
|
+
Options:
|
|
119
|
+
--global, -g Apply to global settings
|
|
120
|
+
/path/to/repo Apply to specific project
|
|
121
|
+
```
|
|
142
122
|
|
|
143
|
-
|
|
144
|
-
bun run build
|
|
123
|
+
## How it Works
|
|
145
124
|
|
|
146
|
-
|
|
147
|
-
|
|
125
|
+
```
|
|
126
|
+
Claude tries to stop
|
|
127
|
+
│
|
|
128
|
+
▼
|
|
129
|
+
Stop hook fires
|
|
130
|
+
│
|
|
131
|
+
▼
|
|
132
|
+
Read config from settings.json
|
|
133
|
+
│
|
|
134
|
+
▼
|
|
135
|
+
Check matching task lists
|
|
136
|
+
│
|
|
137
|
+
├── Tasks remaining → BLOCK stop, prompt to continue
|
|
138
|
+
│
|
|
139
|
+
└── All complete → ALLOW stop
|
|
148
140
|
```
|
|
149
141
|
|
|
150
142
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -30,12 +30,37 @@ function readStdinJson() {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
function readSettings(path) {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return {};
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getConfig(cwd) {
|
|
43
|
+
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
44
|
+
if (projectSettings[CONFIG_KEY]) {
|
|
45
|
+
return projectSettings[CONFIG_KEY];
|
|
46
|
+
}
|
|
47
|
+
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
48
|
+
if (globalSettings[CONFIG_KEY]) {
|
|
49
|
+
return globalSettings[CONFIG_KEY];
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
|
|
53
|
+
keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
|
|
54
|
+
enabled: process.env.CHECK_TASKS_DISABLED !== "1"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
33
57
|
function getSessionName(transcriptPath) {
|
|
34
58
|
if (!existsSync(transcriptPath))
|
|
35
59
|
return null;
|
|
36
60
|
try {
|
|
37
61
|
const content = readFileSync(transcriptPath, "utf-8");
|
|
38
|
-
let
|
|
62
|
+
let customTitle = null;
|
|
63
|
+
let slug = null;
|
|
39
64
|
let searchStart = 0;
|
|
40
65
|
while (true) {
|
|
41
66
|
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
@@ -49,16 +74,60 @@ function getSessionName(transcriptPath) {
|
|
|
49
74
|
try {
|
|
50
75
|
const entry = JSON.parse(line);
|
|
51
76
|
if (entry.type === "custom-title" && entry.customTitle) {
|
|
52
|
-
|
|
77
|
+
customTitle = entry.customTitle;
|
|
53
78
|
}
|
|
54
79
|
} catch {}
|
|
55
80
|
searchStart = titleIndex + 1;
|
|
56
81
|
}
|
|
57
|
-
|
|
82
|
+
if (!customTitle) {
|
|
83
|
+
const slugMatch = content.match(/"slug"\s*:\s*"([^"]+)"/);
|
|
84
|
+
if (slugMatch) {
|
|
85
|
+
slug = slugMatch[1];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return customTitle || slug;
|
|
58
89
|
} catch {
|
|
59
90
|
return null;
|
|
60
91
|
}
|
|
61
92
|
}
|
|
93
|
+
function getAllTaskLists() {
|
|
94
|
+
const tasksDir = join(homedir(), ".claude", "tasks");
|
|
95
|
+
if (!existsSync(tasksDir))
|
|
96
|
+
return [];
|
|
97
|
+
try {
|
|
98
|
+
return readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function getProjectTaskLists(cwd) {
|
|
104
|
+
const allLists = getAllTaskLists();
|
|
105
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
106
|
+
const projectLists = allLists.filter((list) => {
|
|
107
|
+
const listLower = list.toLowerCase();
|
|
108
|
+
const dirLower = dirName.toLowerCase();
|
|
109
|
+
if (listLower.startsWith(dirLower + "-"))
|
|
110
|
+
return true;
|
|
111
|
+
if (listLower === dirLower)
|
|
112
|
+
return true;
|
|
113
|
+
return false;
|
|
114
|
+
});
|
|
115
|
+
return projectLists;
|
|
116
|
+
}
|
|
117
|
+
function getTasksFromList(listId) {
|
|
118
|
+
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
119
|
+
if (!existsSync(tasksDir))
|
|
120
|
+
return [];
|
|
121
|
+
try {
|
|
122
|
+
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
123
|
+
return taskFiles.map((file) => {
|
|
124
|
+
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
125
|
+
return JSON.parse(content);
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
62
131
|
function approve() {
|
|
63
132
|
console.log(JSON.stringify({ decision: "approve" }));
|
|
64
133
|
process.exit(0);
|
|
@@ -68,121 +137,154 @@ function block(reason) {
|
|
|
68
137
|
process.exit(0);
|
|
69
138
|
}
|
|
70
139
|
function run() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!taskListId) {
|
|
140
|
+
const hookInput = readStdinJson();
|
|
141
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
142
|
+
const config = getConfig(cwd);
|
|
143
|
+
if (config.enabled === false) {
|
|
76
144
|
approve();
|
|
77
145
|
}
|
|
78
|
-
const hookInput = readStdinJson();
|
|
79
146
|
let sessionName = null;
|
|
80
147
|
if (hookInput?.transcript_path) {
|
|
81
148
|
sessionName = getSessionName(hookInput.transcript_path);
|
|
82
149
|
}
|
|
83
|
-
const nameToCheck = sessionName || taskListId || "";
|
|
84
|
-
const keywords =
|
|
85
|
-
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
|
|
86
|
-
if (!matchesKeyword) {
|
|
150
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
151
|
+
const keywords = config.keywords || ["dev"];
|
|
152
|
+
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
|
|
153
|
+
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
87
154
|
approve();
|
|
88
155
|
}
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
156
|
+
let listsToCheck = [];
|
|
157
|
+
if (config.taskListId) {
|
|
158
|
+
listsToCheck = [config.taskListId];
|
|
159
|
+
} else {
|
|
160
|
+
const projectLists = getProjectTaskLists(cwd);
|
|
161
|
+
if (projectLists.length > 0) {
|
|
162
|
+
if (keywords.length > 0) {
|
|
163
|
+
listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
|
|
164
|
+
} else {
|
|
165
|
+
listsToCheck = projectLists;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
92
168
|
}
|
|
93
|
-
|
|
94
|
-
if (taskFiles.length === 0) {
|
|
169
|
+
if (listsToCheck.length === 0) {
|
|
95
170
|
approve();
|
|
96
171
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
172
|
+
let allPending = [];
|
|
173
|
+
let allInProgress = [];
|
|
174
|
+
let allCompleted = [];
|
|
175
|
+
let activeListId = null;
|
|
176
|
+
for (const listId of listsToCheck) {
|
|
177
|
+
const tasks = getTasksFromList(listId);
|
|
178
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
179
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
180
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
181
|
+
if (pending.length > 0 || inProgress.length > 0) {
|
|
182
|
+
activeListId = listId;
|
|
183
|
+
}
|
|
184
|
+
allPending.push(...pending);
|
|
185
|
+
allInProgress.push(...inProgress);
|
|
186
|
+
allCompleted.push(...completed);
|
|
187
|
+
}
|
|
188
|
+
const remainingCount = allPending.length + allInProgress.length;
|
|
105
189
|
if (remainingCount > 0) {
|
|
106
|
-
const nextTasks =
|
|
190
|
+
const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
|
|
107
191
|
`);
|
|
192
|
+
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
108
193
|
const prompt = `
|
|
109
|
-
STOP BLOCKED: You have ${remainingCount} tasks remaining (${
|
|
194
|
+
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
110
195
|
|
|
111
|
-
|
|
196
|
+
⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
|
|
197
|
+
|
|
198
|
+
You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
|
|
112
199
|
|
|
113
200
|
Next pending tasks:
|
|
114
201
|
${nextTasks}
|
|
115
|
-
${
|
|
202
|
+
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
116
203
|
|
|
117
|
-
INSTRUCTIONS:
|
|
204
|
+
MANDATORY INSTRUCTIONS (follow these NOW):
|
|
118
205
|
1. Use TaskList to see all tasks
|
|
119
|
-
2.
|
|
120
|
-
3. Use TaskUpdate to mark it as in_progress
|
|
121
|
-
4. Complete the task
|
|
122
|
-
5. Use TaskUpdate to mark it as completed
|
|
123
|
-
6.
|
|
206
|
+
2. Use TaskGet to read the FIRST pending task's full description
|
|
207
|
+
3. Use TaskUpdate to mark it as in_progress BEFORE starting work
|
|
208
|
+
4. Complete the task (write code, run commands, etc.)
|
|
209
|
+
5. Use TaskUpdate to mark it as completed AFTER finishing
|
|
210
|
+
6. IMMEDIATELY move to the next task - DO NOT STOP
|
|
211
|
+
|
|
212
|
+
CRITICAL RULES:
|
|
213
|
+
- NEVER ask "would you like me to..." - just DO IT
|
|
214
|
+
- NEVER ask for confirmation - just WORK
|
|
215
|
+
- NEVER stop to explain what you'll do - just DO IT
|
|
216
|
+
- If a task is unclear, make reasonable assumptions and proceed
|
|
217
|
+
- If you encounter an error, fix it and continue
|
|
218
|
+
- Keep working until remainingCount = 0
|
|
124
219
|
|
|
125
|
-
|
|
220
|
+
START WORKING NOW. Use TaskList tool in your next response.
|
|
126
221
|
`.trim();
|
|
127
222
|
block(prompt);
|
|
128
223
|
}
|
|
129
224
|
approve();
|
|
130
225
|
}
|
|
226
|
+
var CONFIG_KEY = "checkTasksConfig";
|
|
131
227
|
var init_hook = __esm(() => {
|
|
132
228
|
if (false) {}
|
|
133
229
|
});
|
|
134
230
|
|
|
135
231
|
// src/cli.ts
|
|
136
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
137
|
-
import { join as join2, dirname } from "path";
|
|
232
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync as readdirSync2 } from "fs";
|
|
233
|
+
import { join as join2, dirname, resolve } from "path";
|
|
138
234
|
import { homedir as homedir2 } from "os";
|
|
235
|
+
import * as readline from "readline";
|
|
139
236
|
var PACKAGE_NAME = "@hasnaxyz/hook-checktasks";
|
|
140
|
-
var
|
|
237
|
+
var CONFIG_KEY2 = "checkTasksConfig";
|
|
238
|
+
var c = {
|
|
141
239
|
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
142
240
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
143
241
|
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
144
242
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
243
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
145
244
|
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
146
245
|
};
|
|
147
246
|
function printUsage() {
|
|
148
247
|
console.log(`
|
|
149
|
-
${
|
|
248
|
+
${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
|
|
150
249
|
|
|
151
|
-
${
|
|
152
|
-
|
|
250
|
+
${c.bold("USAGE:")}
|
|
251
|
+
hook-checktasks install [path] Install the hook
|
|
252
|
+
hook-checktasks config [path] Update configuration
|
|
253
|
+
hook-checktasks uninstall [path] Remove the hook
|
|
254
|
+
hook-checktasks status Show hook status
|
|
255
|
+
hook-checktasks run Execute hook ${c.dim("(called by Claude Code)")}
|
|
153
256
|
|
|
154
|
-
${
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
257
|
+
${c.bold("OPTIONS:")}
|
|
258
|
+
${c.dim("(no args)")} Auto-detect: if in git repo \u2192 install there, else \u2192 prompt
|
|
259
|
+
--global, -g Apply to ~/.claude/settings.json
|
|
260
|
+
--task-list-id, -t <id> Task list ID (non-interactive)
|
|
261
|
+
--keywords, -k <k1,k2> Keywords, comma-separated (non-interactive)
|
|
262
|
+
--yes, -y Non-interactive mode, use defaults
|
|
263
|
+
/path/to/repo Apply to specific project path
|
|
159
264
|
|
|
160
|
-
${
|
|
161
|
-
|
|
162
|
-
|
|
265
|
+
${c.bold("EXAMPLES:")}
|
|
266
|
+
hook-checktasks install ${c.dim("# Install with config prompts")}
|
|
267
|
+
hook-checktasks install --global ${c.dim("# Global install")}
|
|
268
|
+
hook-checktasks install -t myproject-dev -y ${c.dim("# Non-interactive")}
|
|
269
|
+
hook-checktasks config ${c.dim("# Update task list ID, keywords")}
|
|
270
|
+
hook-checktasks status ${c.dim("# Check what's installed")}
|
|
163
271
|
|
|
164
|
-
${
|
|
165
|
-
|
|
166
|
-
bunx ${PACKAGE_NAME} install # Install in current project
|
|
167
|
-
bunx ${PACKAGE_NAME} uninstall --global # Remove from global settings
|
|
168
|
-
bunx ${PACKAGE_NAME} status # Show hook status
|
|
169
|
-
|
|
170
|
-
${colors.bold("ENVIRONMENT VARIABLES:")}
|
|
171
|
-
CLAUDE_CODE_TASK_LIST_ID Task list to monitor (required for task checking)
|
|
172
|
-
CHECK_TASKS_KEYWORDS Keywords to trigger check (default: "dev")
|
|
173
|
-
CHECK_TASKS_DISABLED Set to "1" to disable the hook
|
|
272
|
+
${c.bold("GLOBAL CLI INSTALL:")}
|
|
273
|
+
bun add -g ${PACKAGE_NAME}
|
|
174
274
|
`);
|
|
175
275
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
276
|
+
function isGitRepo(path) {
|
|
277
|
+
return existsSync2(join2(path, ".git"));
|
|
278
|
+
}
|
|
279
|
+
function getSettingsPath(targetPath) {
|
|
280
|
+
if (targetPath === "global") {
|
|
178
281
|
return join2(homedir2(), ".claude", "settings.json");
|
|
179
282
|
}
|
|
180
|
-
return join2(
|
|
283
|
+
return join2(targetPath, ".claude", "settings.json");
|
|
181
284
|
}
|
|
182
|
-
function
|
|
183
|
-
if (!existsSync2(path))
|
|
285
|
+
function readSettings2(path) {
|
|
286
|
+
if (!existsSync2(path))
|
|
184
287
|
return {};
|
|
185
|
-
}
|
|
186
288
|
try {
|
|
187
289
|
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
188
290
|
} catch {
|
|
@@ -195,7 +297,7 @@ function writeSettings(path, settings) {
|
|
|
195
297
|
`);
|
|
196
298
|
}
|
|
197
299
|
function getHookCommand() {
|
|
198
|
-
return `bunx ${PACKAGE_NAME} run`;
|
|
300
|
+
return `bunx ${PACKAGE_NAME}@latest run`;
|
|
199
301
|
}
|
|
200
302
|
function hookExists(settings) {
|
|
201
303
|
const hooks = settings.hooks;
|
|
@@ -204,15 +306,21 @@ function hookExists(settings) {
|
|
|
204
306
|
const stopHooks = hooks.Stop;
|
|
205
307
|
return stopHooks.some((group) => group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME)));
|
|
206
308
|
}
|
|
309
|
+
function getConfig2(settings) {
|
|
310
|
+
return settings[CONFIG_KEY2] || {};
|
|
311
|
+
}
|
|
312
|
+
function setConfig(settings, config) {
|
|
313
|
+
settings[CONFIG_KEY2] = config;
|
|
314
|
+
return settings;
|
|
315
|
+
}
|
|
207
316
|
function addHook(settings) {
|
|
208
317
|
const hookConfig = {
|
|
209
318
|
type: "command",
|
|
210
319
|
command: getHookCommand(),
|
|
211
320
|
timeout: 120
|
|
212
321
|
};
|
|
213
|
-
if (!settings.hooks)
|
|
322
|
+
if (!settings.hooks)
|
|
214
323
|
settings.hooks = {};
|
|
215
|
-
}
|
|
216
324
|
const hooks = settings.hooks;
|
|
217
325
|
if (!hooks.Stop) {
|
|
218
326
|
hooks.Stop = [{ hooks: [hookConfig] }];
|
|
@@ -237,96 +345,325 @@ function removeHook(settings) {
|
|
|
237
345
|
}
|
|
238
346
|
}
|
|
239
347
|
hooks.Stop = stopHooks.filter((g) => g.hooks && g.hooks.length > 0);
|
|
240
|
-
if (hooks.Stop.length === 0)
|
|
348
|
+
if (hooks.Stop.length === 0)
|
|
241
349
|
delete hooks.Stop;
|
|
242
|
-
|
|
350
|
+
delete settings[CONFIG_KEY2];
|
|
243
351
|
return settings;
|
|
244
352
|
}
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
|
|
353
|
+
async function prompt(question) {
|
|
354
|
+
const rl = readline.createInterface({
|
|
355
|
+
input: process.stdin,
|
|
356
|
+
output: process.stdout
|
|
357
|
+
});
|
|
358
|
+
return new Promise((resolve2) => {
|
|
359
|
+
rl.question(question, (answer) => {
|
|
360
|
+
rl.close();
|
|
361
|
+
resolve2(answer.trim());
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function getAllTaskLists2() {
|
|
366
|
+
const tasksDir = join2(homedir2(), ".claude", "tasks");
|
|
367
|
+
if (!existsSync2(tasksDir))
|
|
368
|
+
return [];
|
|
369
|
+
try {
|
|
370
|
+
return readdirSync2(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
371
|
+
} catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function getProjectTaskLists2(projectPath) {
|
|
376
|
+
const allLists = getAllTaskLists2();
|
|
377
|
+
const dirName = projectPath.split("/").filter(Boolean).pop() || "";
|
|
378
|
+
const projectLists = allLists.filter((list) => {
|
|
379
|
+
const listLower = list.toLowerCase();
|
|
380
|
+
const dirLower = dirName.toLowerCase();
|
|
381
|
+
if (listLower.startsWith(dirLower + "-"))
|
|
382
|
+
return true;
|
|
383
|
+
if (listLower.includes(dirLower))
|
|
384
|
+
return true;
|
|
385
|
+
return false;
|
|
386
|
+
});
|
|
387
|
+
return projectLists;
|
|
388
|
+
}
|
|
389
|
+
function parseInstallArgs(args) {
|
|
390
|
+
const options = {};
|
|
391
|
+
for (let i = 0;i < args.length; i++) {
|
|
392
|
+
const arg = args[i];
|
|
393
|
+
if (arg === "--global" || arg === "-g") {
|
|
394
|
+
options.global = true;
|
|
395
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
396
|
+
options.yes = true;
|
|
397
|
+
} else if (arg === "--task-list-id" || arg === "-t") {
|
|
398
|
+
options.taskListId = args[++i];
|
|
399
|
+
} else if (arg === "--keywords" || arg === "-k") {
|
|
400
|
+
options.keywords = args[++i]?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
401
|
+
} else if (!arg.startsWith("-")) {
|
|
402
|
+
options.path = arg;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return options;
|
|
406
|
+
}
|
|
407
|
+
async function resolveTarget(args) {
|
|
408
|
+
if (args.includes("--global") || args.includes("-g")) {
|
|
409
|
+
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
410
|
+
}
|
|
411
|
+
const pathArg = args.find((a) => !a.startsWith("-"));
|
|
412
|
+
if (pathArg) {
|
|
413
|
+
const fullPath = resolve(pathArg);
|
|
414
|
+
if (!existsSync2(fullPath)) {
|
|
415
|
+
console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
return { path: fullPath, label: `project (${fullPath})` };
|
|
419
|
+
}
|
|
420
|
+
const cwd = process.cwd();
|
|
421
|
+
if (isGitRepo(cwd)) {
|
|
422
|
+
console.log(c.green("\u2713"), `Detected git repo: ${c.cyan(cwd)}`);
|
|
423
|
+
return { path: cwd, label: `project (${cwd})` };
|
|
424
|
+
}
|
|
425
|
+
console.log(c.yellow("!"), `Current directory: ${c.cyan(cwd)}`);
|
|
426
|
+
console.log(c.dim(` (not a git repository)
|
|
427
|
+
`));
|
|
428
|
+
console.log(`Where would you like to install?
|
|
429
|
+
`);
|
|
430
|
+
console.log(" 1. Here", c.dim(`(${cwd})`));
|
|
431
|
+
console.log(" 2. Global", c.dim("(~/.claude/settings.json)"));
|
|
432
|
+
console.log(` 3. Enter a different path
|
|
433
|
+
`);
|
|
434
|
+
const choice = await prompt("Choice (1/2/3): ");
|
|
435
|
+
if (choice === "1") {
|
|
436
|
+
return { path: cwd, label: `project (${cwd})` };
|
|
437
|
+
} else if (choice === "2") {
|
|
438
|
+
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
439
|
+
} else if (choice === "3") {
|
|
440
|
+
const inputPath = await prompt("Path: ");
|
|
441
|
+
if (!inputPath) {
|
|
442
|
+
console.log(c.red("\u2717"), "No path provided");
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
const fullPath = resolve(inputPath);
|
|
446
|
+
if (!existsSync2(fullPath)) {
|
|
447
|
+
console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return { path: fullPath, label: `project (${fullPath})` };
|
|
451
|
+
} else {
|
|
452
|
+
console.log(c.red("\u2717"), "Invalid choice");
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function promptForConfig(existingConfig = {}, projectPath) {
|
|
457
|
+
const config = { ...existingConfig };
|
|
458
|
+
const availableLists = projectPath ? getProjectTaskLists2(projectPath) : getAllTaskLists2();
|
|
248
459
|
console.log(`
|
|
249
|
-
${
|
|
460
|
+
${c.bold("Configuration")}
|
|
250
461
|
`);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(
|
|
462
|
+
console.log(c.bold("Task List ID:"));
|
|
463
|
+
if (availableLists.length > 0) {
|
|
464
|
+
console.log(c.dim(" Available lists for this project:"));
|
|
465
|
+
availableLists.forEach((list, i) => {
|
|
466
|
+
console.log(c.dim(` ${i + 1}. ${list}`));
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
console.log(c.dim(" No task lists found for this project"));
|
|
470
|
+
}
|
|
471
|
+
console.log(c.dim(` Leave empty to check all matching lists
|
|
472
|
+
`));
|
|
473
|
+
const currentList = config.taskListId || "(all lists)";
|
|
474
|
+
const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
|
|
475
|
+
if (listInput) {
|
|
476
|
+
const num = parseInt(listInput, 10);
|
|
477
|
+
if (!isNaN(num) && num > 0 && num <= availableLists.length) {
|
|
478
|
+
config.taskListId = availableLists[num - 1];
|
|
479
|
+
} else {
|
|
480
|
+
config.taskListId = listInput;
|
|
481
|
+
}
|
|
482
|
+
} else if (!existingConfig.taskListId) {
|
|
483
|
+
config.taskListId = undefined;
|
|
484
|
+
}
|
|
485
|
+
const currentKeywords = config.keywords?.join(", ") || "dev";
|
|
486
|
+
const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
|
|
487
|
+
if (keywordsInput) {
|
|
488
|
+
config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
489
|
+
} else if (!existingConfig.keywords) {
|
|
490
|
+
config.keywords = ["dev"];
|
|
491
|
+
}
|
|
492
|
+
config.enabled = true;
|
|
493
|
+
return config;
|
|
494
|
+
}
|
|
495
|
+
async function install(args) {
|
|
496
|
+
console.log(`
|
|
497
|
+
${c.bold("hook-checktasks install")}
|
|
254
498
|
`);
|
|
499
|
+
const options = parseInstallArgs(args);
|
|
500
|
+
let target = null;
|
|
501
|
+
if (options.global) {
|
|
502
|
+
target = { path: "global", label: "global (~/.claude/settings.json)" };
|
|
503
|
+
} else if (options.path) {
|
|
504
|
+
const fullPath = resolve(options.path);
|
|
505
|
+
if (!existsSync2(fullPath)) {
|
|
506
|
+
console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
target = { path: fullPath, label: `project (${fullPath})` };
|
|
510
|
+
} else if (options.yes) {
|
|
511
|
+
const cwd = process.cwd();
|
|
512
|
+
target = { path: cwd, label: `project (${cwd})` };
|
|
513
|
+
} else {
|
|
514
|
+
target = await resolveTarget(args);
|
|
255
515
|
}
|
|
256
|
-
|
|
516
|
+
if (!target)
|
|
517
|
+
return;
|
|
518
|
+
const settingsPath = getSettingsPath(target.path);
|
|
519
|
+
let settings = readSettings2(settingsPath);
|
|
257
520
|
if (hookExists(settings)) {
|
|
258
|
-
console.log(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
521
|
+
console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
|
|
522
|
+
if (!options.yes) {
|
|
523
|
+
const update = await prompt("Update configuration? (y/n): ");
|
|
524
|
+
if (update.toLowerCase() !== "y")
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
settings = addHook(settings);
|
|
529
|
+
}
|
|
530
|
+
const existingConfig = getConfig2(settings);
|
|
531
|
+
let config;
|
|
532
|
+
if (options.yes || options.taskListId || options.keywords) {
|
|
533
|
+
config = {
|
|
534
|
+
...existingConfig,
|
|
535
|
+
taskListId: options.taskListId || existingConfig.taskListId,
|
|
536
|
+
keywords: options.keywords || existingConfig.keywords || ["dev"],
|
|
537
|
+
enabled: true
|
|
538
|
+
};
|
|
539
|
+
} else {
|
|
540
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
541
|
+
config = await promptForConfig(existingConfig, projectPath);
|
|
542
|
+
}
|
|
543
|
+
settings = setConfig(settings, config);
|
|
544
|
+
writeSettings(settingsPath, settings);
|
|
545
|
+
console.log();
|
|
546
|
+
console.log(c.green("\u2713"), `Installed to ${target.label}`);
|
|
547
|
+
console.log();
|
|
548
|
+
console.log(c.bold("Configuration:"));
|
|
549
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
550
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
551
|
+
console.log();
|
|
552
|
+
}
|
|
553
|
+
async function configure(args) {
|
|
554
|
+
console.log(`
|
|
555
|
+
${c.bold("hook-checktasks config")}
|
|
262
556
|
`);
|
|
557
|
+
const target = await resolveTarget(args);
|
|
558
|
+
if (!target)
|
|
559
|
+
return;
|
|
560
|
+
const settingsPath = getSettingsPath(target.path);
|
|
561
|
+
if (!existsSync2(settingsPath)) {
|
|
562
|
+
console.log(c.red("\u2717"), `No settings file at ${settingsPath}`);
|
|
563
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
263
564
|
return;
|
|
264
565
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
566
|
+
let settings = readSettings2(settingsPath);
|
|
567
|
+
if (!hookExists(settings)) {
|
|
568
|
+
console.log(c.red("\u2717"), `Hook not installed in ${target.label}`);
|
|
569
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const existingConfig = getConfig2(settings);
|
|
573
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
574
|
+
const config = await promptForConfig(existingConfig, projectPath);
|
|
575
|
+
settings = setConfig(settings, config);
|
|
576
|
+
writeSettings(settingsPath, settings);
|
|
268
577
|
console.log();
|
|
269
|
-
console.log(
|
|
270
|
-
console.log(" CLAUDE_CODE_TASK_LIST_ID Task list to monitor (required)");
|
|
271
|
-
console.log(" CHECK_TASKS_KEYWORDS Keywords to trigger check (default: 'dev')");
|
|
272
|
-
console.log(" CHECK_TASKS_DISABLED Set to '1' to disable the hook");
|
|
578
|
+
console.log(c.green("\u2713"), `Configuration updated`);
|
|
273
579
|
console.log();
|
|
274
|
-
console.log(
|
|
275
|
-
console.log(
|
|
580
|
+
console.log(c.bold("New configuration:"));
|
|
581
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
582
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
276
583
|
console.log();
|
|
277
584
|
}
|
|
278
|
-
function uninstall(
|
|
279
|
-
const scope = global ? "global" : "project";
|
|
280
|
-
const settingsPath = getSettingsPath(global);
|
|
585
|
+
async function uninstall(args) {
|
|
281
586
|
console.log(`
|
|
282
|
-
${
|
|
587
|
+
${c.bold("hook-checktasks uninstall")}
|
|
283
588
|
`);
|
|
589
|
+
const target = await resolveTarget(args);
|
|
590
|
+
if (!target)
|
|
591
|
+
return;
|
|
592
|
+
const settingsPath = getSettingsPath(target.path);
|
|
284
593
|
if (!existsSync2(settingsPath)) {
|
|
285
|
-
console.log(
|
|
594
|
+
console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
|
|
286
595
|
return;
|
|
287
596
|
}
|
|
288
|
-
const settings =
|
|
597
|
+
const settings = readSettings2(settingsPath);
|
|
289
598
|
if (!hookExists(settings)) {
|
|
290
|
-
console.log(
|
|
599
|
+
console.log(c.yellow("!"), `Hook not found in ${target.label}`);
|
|
291
600
|
return;
|
|
292
601
|
}
|
|
293
|
-
const
|
|
294
|
-
writeSettings(settingsPath,
|
|
295
|
-
console.log(
|
|
296
|
-
console.log();
|
|
602
|
+
const updated = removeHook(settings);
|
|
603
|
+
writeSettings(settingsPath, updated);
|
|
604
|
+
console.log(c.green("\u2713"), `Removed from ${target.label}`);
|
|
297
605
|
}
|
|
298
606
|
function status() {
|
|
299
607
|
console.log(`
|
|
300
|
-
${
|
|
608
|
+
${c.bold("hook-checktasks status")}
|
|
301
609
|
`);
|
|
302
|
-
const globalPath = getSettingsPath(
|
|
303
|
-
const
|
|
304
|
-
const globalSettings = readSettings(globalPath);
|
|
610
|
+
const globalPath = getSettingsPath("global");
|
|
611
|
+
const globalSettings = readSettings2(globalPath);
|
|
305
612
|
const globalInstalled = hookExists(globalSettings);
|
|
306
|
-
|
|
613
|
+
const globalConfig = getConfig2(globalSettings);
|
|
614
|
+
console.log(globalInstalled ? c.green("\u2713") : c.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", c.dim(`(${globalPath})`));
|
|
615
|
+
if (globalInstalled) {
|
|
616
|
+
console.log(c.dim(` List: ${globalConfig.taskListId || "(all)"}, Keywords: ${globalConfig.keywords?.join(", ") || "dev"}`));
|
|
617
|
+
}
|
|
618
|
+
const cwd = process.cwd();
|
|
619
|
+
const projectPath = getSettingsPath(cwd);
|
|
307
620
|
if (existsSync2(projectPath)) {
|
|
308
|
-
const projectSettings =
|
|
621
|
+
const projectSettings = readSettings2(projectPath);
|
|
309
622
|
const projectInstalled = hookExists(projectSettings);
|
|
310
|
-
|
|
623
|
+
const projectConfig = getConfig2(projectSettings);
|
|
624
|
+
console.log(projectInstalled ? c.green("\u2713") : c.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", c.dim(`(${projectPath})`));
|
|
625
|
+
if (projectInstalled) {
|
|
626
|
+
console.log(c.dim(` List: ${projectConfig.taskListId || "(all)"}, Keywords: ${projectConfig.keywords?.join(", ") || "dev"}`));
|
|
627
|
+
}
|
|
311
628
|
} else {
|
|
312
|
-
console.log(
|
|
629
|
+
console.log(c.dim("\xB7"), "Project:", c.dim("No .claude/settings.json"));
|
|
630
|
+
}
|
|
631
|
+
const projectLists = getProjectTaskLists2(cwd);
|
|
632
|
+
if (projectLists.length > 0) {
|
|
633
|
+
console.log();
|
|
634
|
+
console.log(c.bold("Task lists for this project:"));
|
|
635
|
+
projectLists.forEach((list) => console.log(c.dim(` - ${list}`)));
|
|
636
|
+
} else {
|
|
637
|
+
const allLists = getAllTaskLists2();
|
|
638
|
+
if (allLists.length > 0) {
|
|
639
|
+
console.log();
|
|
640
|
+
console.log(c.bold("All task lists:"), c.dim("(none match this project)"));
|
|
641
|
+
allLists.slice(0, 10).forEach((list) => console.log(c.dim(` - ${list}`)));
|
|
642
|
+
if (allLists.length > 10) {
|
|
643
|
+
console.log(c.dim(` ... and ${allLists.length - 10} more`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const envTaskList = process.env.CLAUDE_CODE_TASK_LIST_ID;
|
|
648
|
+
if (envTaskList) {
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(c.bold("Environment (legacy):"));
|
|
651
|
+
console.log(` CLAUDE_CODE_TASK_LIST_ID: ${envTaskList}`);
|
|
313
652
|
}
|
|
314
|
-
console.log();
|
|
315
|
-
console.log(colors.bold("Environment:"));
|
|
316
|
-
console.log(" CLAUDE_CODE_TASK_LIST_ID:", process.env.CLAUDE_CODE_TASK_LIST_ID || colors.yellow("(not set)"));
|
|
317
|
-
console.log(" CHECK_TASKS_KEYWORDS:", process.env.CHECK_TASKS_KEYWORDS || "dev (default)");
|
|
318
|
-
console.log(" CHECK_TASKS_DISABLED:", process.env.CHECK_TASKS_DISABLED === "1" ? colors.yellow("yes") : "no");
|
|
319
653
|
console.log();
|
|
320
654
|
}
|
|
321
655
|
var args = process.argv.slice(2);
|
|
322
656
|
var command = args[0];
|
|
323
|
-
var
|
|
657
|
+
var commandArgs = args.slice(1);
|
|
324
658
|
switch (command) {
|
|
325
659
|
case "install":
|
|
326
|
-
install(
|
|
660
|
+
install(commandArgs);
|
|
661
|
+
break;
|
|
662
|
+
case "config":
|
|
663
|
+
configure(commandArgs);
|
|
327
664
|
break;
|
|
328
665
|
case "uninstall":
|
|
329
|
-
uninstall(
|
|
666
|
+
uninstall(commandArgs);
|
|
330
667
|
break;
|
|
331
668
|
case "run":
|
|
332
669
|
Promise.resolve().then(() => (init_hook(), exports_hook)).then((m) => m.run());
|
|
@@ -340,7 +677,7 @@ switch (command) {
|
|
|
340
677
|
printUsage();
|
|
341
678
|
break;
|
|
342
679
|
default:
|
|
343
|
-
console.error(
|
|
680
|
+
console.error(c.red(`Unknown command: ${command}`));
|
|
344
681
|
printUsage();
|
|
345
682
|
process.exit(1);
|
|
346
683
|
}
|
package/dist/hook.js
CHANGED
|
@@ -17,6 +17,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
17
17
|
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
18
18
|
import { join } from "path";
|
|
19
19
|
import { homedir } from "os";
|
|
20
|
+
var CONFIG_KEY = "checkTasksConfig";
|
|
20
21
|
function readStdinJson() {
|
|
21
22
|
try {
|
|
22
23
|
const stdin = readFileSync(0, "utf-8");
|
|
@@ -25,12 +26,37 @@ function readStdinJson() {
|
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
function readSettings(path) {
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return {};
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getConfig(cwd) {
|
|
39
|
+
const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
|
|
40
|
+
if (projectSettings[CONFIG_KEY]) {
|
|
41
|
+
return projectSettings[CONFIG_KEY];
|
|
42
|
+
}
|
|
43
|
+
const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
|
|
44
|
+
if (globalSettings[CONFIG_KEY]) {
|
|
45
|
+
return globalSettings[CONFIG_KEY];
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
|
|
49
|
+
keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
|
|
50
|
+
enabled: process.env.CHECK_TASKS_DISABLED !== "1"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
28
53
|
function getSessionName(transcriptPath) {
|
|
29
54
|
if (!existsSync(transcriptPath))
|
|
30
55
|
return null;
|
|
31
56
|
try {
|
|
32
57
|
const content = readFileSync(transcriptPath, "utf-8");
|
|
33
|
-
let
|
|
58
|
+
let customTitle = null;
|
|
59
|
+
let slug = null;
|
|
34
60
|
let searchStart = 0;
|
|
35
61
|
while (true) {
|
|
36
62
|
const titleIndex = content.indexOf('"custom-title"', searchStart);
|
|
@@ -44,16 +70,60 @@ function getSessionName(transcriptPath) {
|
|
|
44
70
|
try {
|
|
45
71
|
const entry = JSON.parse(line);
|
|
46
72
|
if (entry.type === "custom-title" && entry.customTitle) {
|
|
47
|
-
|
|
73
|
+
customTitle = entry.customTitle;
|
|
48
74
|
}
|
|
49
75
|
} catch {}
|
|
50
76
|
searchStart = titleIndex + 1;
|
|
51
77
|
}
|
|
52
|
-
|
|
78
|
+
if (!customTitle) {
|
|
79
|
+
const slugMatch = content.match(/"slug"\s*:\s*"([^"]+)"/);
|
|
80
|
+
if (slugMatch) {
|
|
81
|
+
slug = slugMatch[1];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return customTitle || slug;
|
|
53
85
|
} catch {
|
|
54
86
|
return null;
|
|
55
87
|
}
|
|
56
88
|
}
|
|
89
|
+
function getAllTaskLists() {
|
|
90
|
+
const tasksDir = join(homedir(), ".claude", "tasks");
|
|
91
|
+
if (!existsSync(tasksDir))
|
|
92
|
+
return [];
|
|
93
|
+
try {
|
|
94
|
+
return readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function getProjectTaskLists(cwd) {
|
|
100
|
+
const allLists = getAllTaskLists();
|
|
101
|
+
const dirName = cwd.split("/").filter(Boolean).pop() || "";
|
|
102
|
+
const projectLists = allLists.filter((list) => {
|
|
103
|
+
const listLower = list.toLowerCase();
|
|
104
|
+
const dirLower = dirName.toLowerCase();
|
|
105
|
+
if (listLower.startsWith(dirLower + "-"))
|
|
106
|
+
return true;
|
|
107
|
+
if (listLower === dirLower)
|
|
108
|
+
return true;
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
return projectLists;
|
|
112
|
+
}
|
|
113
|
+
function getTasksFromList(listId) {
|
|
114
|
+
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
115
|
+
if (!existsSync(tasksDir))
|
|
116
|
+
return [];
|
|
117
|
+
try {
|
|
118
|
+
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
119
|
+
return taskFiles.map((file) => {
|
|
120
|
+
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
121
|
+
return JSON.parse(content);
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
57
127
|
function approve() {
|
|
58
128
|
console.log(JSON.stringify({ decision: "approve" }));
|
|
59
129
|
process.exit(0);
|
|
@@ -63,61 +133,87 @@ function block(reason) {
|
|
|
63
133
|
process.exit(0);
|
|
64
134
|
}
|
|
65
135
|
function run() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!taskListId) {
|
|
136
|
+
const hookInput = readStdinJson();
|
|
137
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
138
|
+
const config = getConfig(cwd);
|
|
139
|
+
if (config.enabled === false) {
|
|
71
140
|
approve();
|
|
72
141
|
}
|
|
73
|
-
const hookInput = readStdinJson();
|
|
74
142
|
let sessionName = null;
|
|
75
143
|
if (hookInput?.transcript_path) {
|
|
76
144
|
sessionName = getSessionName(hookInput.transcript_path);
|
|
77
145
|
}
|
|
78
|
-
const nameToCheck = sessionName || taskListId || "";
|
|
79
|
-
const keywords =
|
|
80
|
-
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
|
|
81
|
-
if (!matchesKeyword) {
|
|
146
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
147
|
+
const keywords = config.keywords || ["dev"];
|
|
148
|
+
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
|
|
149
|
+
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
82
150
|
approve();
|
|
83
151
|
}
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
152
|
+
let listsToCheck = [];
|
|
153
|
+
if (config.taskListId) {
|
|
154
|
+
listsToCheck = [config.taskListId];
|
|
155
|
+
} else {
|
|
156
|
+
const projectLists = getProjectTaskLists(cwd);
|
|
157
|
+
if (projectLists.length > 0) {
|
|
158
|
+
if (keywords.length > 0) {
|
|
159
|
+
listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
|
|
160
|
+
} else {
|
|
161
|
+
listsToCheck = projectLists;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
87
164
|
}
|
|
88
|
-
|
|
89
|
-
if (taskFiles.length === 0) {
|
|
165
|
+
if (listsToCheck.length === 0) {
|
|
90
166
|
approve();
|
|
91
167
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
168
|
+
let allPending = [];
|
|
169
|
+
let allInProgress = [];
|
|
170
|
+
let allCompleted = [];
|
|
171
|
+
let activeListId = null;
|
|
172
|
+
for (const listId of listsToCheck) {
|
|
173
|
+
const tasks = getTasksFromList(listId);
|
|
174
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
175
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
176
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
177
|
+
if (pending.length > 0 || inProgress.length > 0) {
|
|
178
|
+
activeListId = listId;
|
|
179
|
+
}
|
|
180
|
+
allPending.push(...pending);
|
|
181
|
+
allInProgress.push(...inProgress);
|
|
182
|
+
allCompleted.push(...completed);
|
|
183
|
+
}
|
|
184
|
+
const remainingCount = allPending.length + allInProgress.length;
|
|
100
185
|
if (remainingCount > 0) {
|
|
101
|
-
const nextTasks =
|
|
186
|
+
const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
|
|
102
187
|
`);
|
|
188
|
+
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
103
189
|
const prompt = `
|
|
104
|
-
STOP BLOCKED: You have ${remainingCount} tasks remaining (${
|
|
190
|
+
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
191
|
+
|
|
192
|
+
⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
|
|
105
193
|
|
|
106
|
-
You MUST continue working
|
|
194
|
+
You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
|
|
107
195
|
|
|
108
196
|
Next pending tasks:
|
|
109
197
|
${nextTasks}
|
|
110
|
-
${
|
|
198
|
+
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
111
199
|
|
|
112
|
-
INSTRUCTIONS:
|
|
200
|
+
MANDATORY INSTRUCTIONS (follow these NOW):
|
|
113
201
|
1. Use TaskList to see all tasks
|
|
114
|
-
2.
|
|
115
|
-
3. Use TaskUpdate to mark it as in_progress
|
|
116
|
-
4. Complete the task
|
|
117
|
-
5. Use TaskUpdate to mark it as completed
|
|
118
|
-
6.
|
|
202
|
+
2. Use TaskGet to read the FIRST pending task's full description
|
|
203
|
+
3. Use TaskUpdate to mark it as in_progress BEFORE starting work
|
|
204
|
+
4. Complete the task (write code, run commands, etc.)
|
|
205
|
+
5. Use TaskUpdate to mark it as completed AFTER finishing
|
|
206
|
+
6. IMMEDIATELY move to the next task - DO NOT STOP
|
|
207
|
+
|
|
208
|
+
CRITICAL RULES:
|
|
209
|
+
- NEVER ask "would you like me to..." - just DO IT
|
|
210
|
+
- NEVER ask for confirmation - just WORK
|
|
211
|
+
- NEVER stop to explain what you'll do - just DO IT
|
|
212
|
+
- If a task is unclear, make reasonable assumptions and proceed
|
|
213
|
+
- If you encounter an error, fix it and continue
|
|
214
|
+
- Keep working until remainingCount = 0
|
|
119
215
|
|
|
120
|
-
|
|
216
|
+
START WORKING NOW. Use TaskList tool in your next response.
|
|
121
217
|
`.trim();
|
|
122
218
|
block(prompt);
|
|
123
219
|
}
|