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