@hasnaxyz/hook-checktasks 0.1.1 → 0.2.0
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 +240 -62
- package/dist/hook.js +89 -26
- 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,30 @@ function getSessionName(transcriptPath) {
|
|
|
59
83
|
return null;
|
|
60
84
|
}
|
|
61
85
|
}
|
|
86
|
+
function getTaskLists() {
|
|
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 getTasksFromList(listId) {
|
|
97
|
+
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
98
|
+
if (!existsSync(tasksDir))
|
|
99
|
+
return [];
|
|
100
|
+
try {
|
|
101
|
+
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
102
|
+
return taskFiles.map((file) => {
|
|
103
|
+
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
104
|
+
return JSON.parse(content);
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
62
110
|
function approve() {
|
|
63
111
|
console.log(JSON.stringify({ decision: "approve" }));
|
|
64
112
|
process.exit(0);
|
|
@@ -68,51 +116,65 @@ function block(reason) {
|
|
|
68
116
|
process.exit(0);
|
|
69
117
|
}
|
|
70
118
|
function run() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!taskListId) {
|
|
119
|
+
const hookInput = readStdinJson();
|
|
120
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
121
|
+
const config = getConfig(cwd);
|
|
122
|
+
if (config.enabled === false) {
|
|
76
123
|
approve();
|
|
77
124
|
}
|
|
78
|
-
const hookInput = readStdinJson();
|
|
79
125
|
let sessionName = null;
|
|
80
126
|
if (hookInput?.transcript_path) {
|
|
81
127
|
sessionName = getSessionName(hookInput.transcript_path);
|
|
82
128
|
}
|
|
83
|
-
const nameToCheck = sessionName || taskListId || "";
|
|
84
|
-
const keywords =
|
|
85
|
-
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
|
|
86
|
-
if (!matchesKeyword) {
|
|
129
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
130
|
+
const keywords = config.keywords || ["dev"];
|
|
131
|
+
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
|
|
132
|
+
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
87
133
|
approve();
|
|
88
134
|
}
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
135
|
+
let listsToCheck = [];
|
|
136
|
+
if (config.taskListId) {
|
|
137
|
+
listsToCheck = [config.taskListId];
|
|
138
|
+
} else {
|
|
139
|
+
const allLists = getTaskLists();
|
|
140
|
+
if (keywords.length > 0) {
|
|
141
|
+
listsToCheck = allLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
|
|
142
|
+
} else {
|
|
143
|
+
listsToCheck = allLists;
|
|
144
|
+
}
|
|
92
145
|
}
|
|
93
|
-
|
|
94
|
-
if (taskFiles.length === 0) {
|
|
146
|
+
if (listsToCheck.length === 0) {
|
|
95
147
|
approve();
|
|
96
148
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
149
|
+
let allPending = [];
|
|
150
|
+
let allInProgress = [];
|
|
151
|
+
let allCompleted = [];
|
|
152
|
+
let activeListId = null;
|
|
153
|
+
for (const listId of listsToCheck) {
|
|
154
|
+
const tasks = getTasksFromList(listId);
|
|
155
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
156
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
157
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
158
|
+
if (pending.length > 0 || inProgress.length > 0) {
|
|
159
|
+
activeListId = listId;
|
|
160
|
+
}
|
|
161
|
+
allPending.push(...pending);
|
|
162
|
+
allInProgress.push(...inProgress);
|
|
163
|
+
allCompleted.push(...completed);
|
|
164
|
+
}
|
|
165
|
+
const remainingCount = allPending.length + allInProgress.length;
|
|
105
166
|
if (remainingCount > 0) {
|
|
106
|
-
const nextTasks =
|
|
167
|
+
const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
|
|
107
168
|
`);
|
|
169
|
+
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
108
170
|
const prompt = `
|
|
109
|
-
STOP BLOCKED: You have ${remainingCount} tasks remaining (${
|
|
171
|
+
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
110
172
|
|
|
111
173
|
You MUST continue working. Do NOT stop until all tasks are completed.
|
|
112
174
|
|
|
113
175
|
Next pending tasks:
|
|
114
176
|
${nextTasks}
|
|
115
|
-
${
|
|
177
|
+
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
116
178
|
|
|
117
179
|
INSTRUCTIONS:
|
|
118
180
|
1. Use TaskList to see all tasks
|
|
@@ -128,16 +190,18 @@ DO NOT STOP. Continue working now.
|
|
|
128
190
|
}
|
|
129
191
|
approve();
|
|
130
192
|
}
|
|
193
|
+
var CONFIG_KEY = "checkTasksConfig";
|
|
131
194
|
var init_hook = __esm(() => {
|
|
132
195
|
if (false) {}
|
|
133
196
|
});
|
|
134
197
|
|
|
135
198
|
// src/cli.ts
|
|
136
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
199
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync as readdirSync2 } from "fs";
|
|
137
200
|
import { join as join2, dirname, resolve } from "path";
|
|
138
201
|
import { homedir as homedir2 } from "os";
|
|
139
202
|
import * as readline from "readline";
|
|
140
203
|
var PACKAGE_NAME = "@hasnaxyz/hook-checktasks";
|
|
204
|
+
var CONFIG_KEY2 = "checkTasksConfig";
|
|
141
205
|
var c = {
|
|
142
206
|
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
143
207
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -152,22 +216,23 @@ ${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
|
|
|
152
216
|
|
|
153
217
|
${c.bold("USAGE:")}
|
|
154
218
|
hook-checktasks install [path] Install the hook
|
|
219
|
+
hook-checktasks config [path] Update configuration
|
|
155
220
|
hook-checktasks uninstall [path] Remove the hook
|
|
156
221
|
hook-checktasks status Show hook status
|
|
157
222
|
hook-checktasks run Execute hook ${c.dim("(called by Claude Code)")}
|
|
158
223
|
|
|
159
|
-
${c.bold("
|
|
224
|
+
${c.bold("OPTIONS:")}
|
|
160
225
|
${c.dim("(no args)")} Auto-detect: if in git repo \u2192 install there, else \u2192 prompt
|
|
161
|
-
--global, -g
|
|
162
|
-
/path/to/repo
|
|
226
|
+
--global, -g Apply to ~/.claude/settings.json
|
|
227
|
+
/path/to/repo Apply to specific project path
|
|
163
228
|
|
|
164
229
|
${c.bold("EXAMPLES:")}
|
|
165
|
-
hook-checktasks install ${c.dim("#
|
|
230
|
+
hook-checktasks install ${c.dim("# Install with config prompts")}
|
|
166
231
|
hook-checktasks install --global ${c.dim("# Global install")}
|
|
167
|
-
hook-checktasks
|
|
232
|
+
hook-checktasks config ${c.dim("# Update task list ID, keywords")}
|
|
168
233
|
hook-checktasks status ${c.dim("# Check what's installed")}
|
|
169
234
|
|
|
170
|
-
${c.bold("GLOBAL INSTALL:")}
|
|
235
|
+
${c.bold("GLOBAL CLI INSTALL:")}
|
|
171
236
|
bun add -g ${PACKAGE_NAME}
|
|
172
237
|
`);
|
|
173
238
|
}
|
|
@@ -180,7 +245,7 @@ function getSettingsPath(targetPath) {
|
|
|
180
245
|
}
|
|
181
246
|
return join2(targetPath, ".claude", "settings.json");
|
|
182
247
|
}
|
|
183
|
-
function
|
|
248
|
+
function readSettings2(path) {
|
|
184
249
|
if (!existsSync2(path))
|
|
185
250
|
return {};
|
|
186
251
|
try {
|
|
@@ -204,6 +269,13 @@ function hookExists(settings) {
|
|
|
204
269
|
const stopHooks = hooks.Stop;
|
|
205
270
|
return stopHooks.some((group) => group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME)));
|
|
206
271
|
}
|
|
272
|
+
function getConfig2(settings) {
|
|
273
|
+
return settings[CONFIG_KEY2] || {};
|
|
274
|
+
}
|
|
275
|
+
function setConfig(settings, config) {
|
|
276
|
+
settings[CONFIG_KEY2] = config;
|
|
277
|
+
return settings;
|
|
278
|
+
}
|
|
207
279
|
function addHook(settings) {
|
|
208
280
|
const hookConfig = {
|
|
209
281
|
type: "command",
|
|
@@ -238,6 +310,7 @@ function removeHook(settings) {
|
|
|
238
310
|
hooks.Stop = stopHooks.filter((g) => g.hooks && g.hooks.length > 0);
|
|
239
311
|
if (hooks.Stop.length === 0)
|
|
240
312
|
delete hooks.Stop;
|
|
313
|
+
delete settings[CONFIG_KEY2];
|
|
241
314
|
return settings;
|
|
242
315
|
}
|
|
243
316
|
async function prompt(question) {
|
|
@@ -252,6 +325,16 @@ async function prompt(question) {
|
|
|
252
325
|
});
|
|
253
326
|
});
|
|
254
327
|
}
|
|
328
|
+
function getAvailableTaskLists() {
|
|
329
|
+
const tasksDir = join2(homedir2(), ".claude", "tasks");
|
|
330
|
+
if (!existsSync2(tasksDir))
|
|
331
|
+
return [];
|
|
332
|
+
try {
|
|
333
|
+
return readdirSync2(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
334
|
+
} catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
255
338
|
async function resolveTarget(args) {
|
|
256
339
|
if (args.includes("--global") || args.includes("-g")) {
|
|
257
340
|
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
@@ -270,17 +353,21 @@ async function resolveTarget(args) {
|
|
|
270
353
|
console.log(c.green("\u2713"), `Detected git repo: ${c.cyan(cwd)}`);
|
|
271
354
|
return { path: cwd, label: `project (${cwd})` };
|
|
272
355
|
}
|
|
273
|
-
console.log(c.yellow("!"), `
|
|
274
|
-
`)
|
|
356
|
+
console.log(c.yellow("!"), `Current directory: ${c.cyan(cwd)}`);
|
|
357
|
+
console.log(c.dim(` (not a git repository)
|
|
358
|
+
`));
|
|
275
359
|
console.log(`Where would you like to install?
|
|
276
360
|
`);
|
|
277
|
-
console.log(" 1.
|
|
278
|
-
console.log(
|
|
361
|
+
console.log(" 1. Here", c.dim(`(${cwd})`));
|
|
362
|
+
console.log(" 2. Global", c.dim("(~/.claude/settings.json)"));
|
|
363
|
+
console.log(` 3. Enter a different path
|
|
279
364
|
`);
|
|
280
|
-
const choice = await prompt("Choice (1/2): ");
|
|
365
|
+
const choice = await prompt("Choice (1/2/3): ");
|
|
281
366
|
if (choice === "1") {
|
|
282
|
-
return { path:
|
|
367
|
+
return { path: cwd, label: `project (${cwd})` };
|
|
283
368
|
} else if (choice === "2") {
|
|
369
|
+
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
370
|
+
} else if (choice === "3") {
|
|
284
371
|
const inputPath = await prompt("Path: ");
|
|
285
372
|
if (!inputPath) {
|
|
286
373
|
console.log(c.red("\u2717"), "No path provided");
|
|
@@ -297,6 +384,43 @@ async function resolveTarget(args) {
|
|
|
297
384
|
return null;
|
|
298
385
|
}
|
|
299
386
|
}
|
|
387
|
+
async function promptForConfig(existingConfig = {}) {
|
|
388
|
+
const config = { ...existingConfig };
|
|
389
|
+
const availableLists = getAvailableTaskLists();
|
|
390
|
+
console.log(`
|
|
391
|
+
${c.bold("Configuration")}
|
|
392
|
+
`);
|
|
393
|
+
console.log(c.bold("Task List ID:"));
|
|
394
|
+
if (availableLists.length > 0) {
|
|
395
|
+
console.log(c.dim(" Available lists:"));
|
|
396
|
+
availableLists.forEach((list, i) => {
|
|
397
|
+
console.log(c.dim(` ${i + 1}. ${list}`));
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
console.log(c.dim(` Leave empty to check ALL task lists
|
|
401
|
+
`));
|
|
402
|
+
const currentList = config.taskListId || "(all lists)";
|
|
403
|
+
const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
|
|
404
|
+
if (listInput) {
|
|
405
|
+
const num = parseInt(listInput, 10);
|
|
406
|
+
if (!isNaN(num) && num > 0 && num <= availableLists.length) {
|
|
407
|
+
config.taskListId = availableLists[num - 1];
|
|
408
|
+
} else {
|
|
409
|
+
config.taskListId = listInput;
|
|
410
|
+
}
|
|
411
|
+
} else if (!existingConfig.taskListId) {
|
|
412
|
+
config.taskListId = undefined;
|
|
413
|
+
}
|
|
414
|
+
const currentKeywords = config.keywords?.join(", ") || "dev";
|
|
415
|
+
const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
|
|
416
|
+
if (keywordsInput) {
|
|
417
|
+
config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
418
|
+
} else if (!existingConfig.keywords) {
|
|
419
|
+
config.keywords = ["dev"];
|
|
420
|
+
}
|
|
421
|
+
config.enabled = true;
|
|
422
|
+
return config;
|
|
423
|
+
}
|
|
300
424
|
async function install(args) {
|
|
301
425
|
console.log(`
|
|
302
426
|
${c.bold("hook-checktasks install")}
|
|
@@ -305,18 +429,57 @@ ${c.bold("hook-checktasks install")}
|
|
|
305
429
|
if (!target)
|
|
306
430
|
return;
|
|
307
431
|
const settingsPath = getSettingsPath(target.path);
|
|
308
|
-
|
|
432
|
+
let settings = readSettings2(settingsPath);
|
|
309
433
|
if (hookExists(settings)) {
|
|
310
|
-
console.log(c.yellow("!"), `
|
|
311
|
-
|
|
434
|
+
console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
|
|
435
|
+
const update = await prompt("Update configuration? (y/n): ");
|
|
436
|
+
if (update.toLowerCase() !== "y")
|
|
437
|
+
return;
|
|
438
|
+
} else {
|
|
439
|
+
settings = addHook(settings);
|
|
312
440
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
console.log(
|
|
318
|
-
console.log(`
|
|
441
|
+
const existingConfig = getConfig2(settings);
|
|
442
|
+
const config = await promptForConfig(existingConfig);
|
|
443
|
+
settings = setConfig(settings, config);
|
|
444
|
+
writeSettings(settingsPath, settings);
|
|
445
|
+
console.log();
|
|
446
|
+
console.log(c.green("\u2713"), `Installed to ${target.label}`);
|
|
447
|
+
console.log();
|
|
448
|
+
console.log(c.bold("Configuration:"));
|
|
449
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
450
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
451
|
+
console.log();
|
|
452
|
+
}
|
|
453
|
+
async function configure(args) {
|
|
454
|
+
console.log(`
|
|
455
|
+
${c.bold("hook-checktasks config")}
|
|
319
456
|
`);
|
|
457
|
+
const target = await resolveTarget(args);
|
|
458
|
+
if (!target)
|
|
459
|
+
return;
|
|
460
|
+
const settingsPath = getSettingsPath(target.path);
|
|
461
|
+
if (!existsSync2(settingsPath)) {
|
|
462
|
+
console.log(c.red("\u2717"), `No settings file at ${settingsPath}`);
|
|
463
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let settings = readSettings2(settingsPath);
|
|
467
|
+
if (!hookExists(settings)) {
|
|
468
|
+
console.log(c.red("\u2717"), `Hook not installed in ${target.label}`);
|
|
469
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const existingConfig = getConfig2(settings);
|
|
473
|
+
const config = await promptForConfig(existingConfig);
|
|
474
|
+
settings = setConfig(settings, config);
|
|
475
|
+
writeSettings(settingsPath, settings);
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(c.green("\u2713"), `Configuration updated`);
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(c.bold("New configuration:"));
|
|
480
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
481
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
482
|
+
console.log();
|
|
320
483
|
}
|
|
321
484
|
async function uninstall(args) {
|
|
322
485
|
console.log(`
|
|
@@ -330,7 +493,7 @@ ${c.bold("hook-checktasks uninstall")}
|
|
|
330
493
|
console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
|
|
331
494
|
return;
|
|
332
495
|
}
|
|
333
|
-
const settings =
|
|
496
|
+
const settings = readSettings2(settingsPath);
|
|
334
497
|
if (!hookExists(settings)) {
|
|
335
498
|
console.log(c.yellow("!"), `Hook not found in ${target.label}`);
|
|
336
499
|
return;
|
|
@@ -344,26 +507,38 @@ function status() {
|
|
|
344
507
|
${c.bold("hook-checktasks status")}
|
|
345
508
|
`);
|
|
346
509
|
const globalPath = getSettingsPath("global");
|
|
347
|
-
const globalSettings =
|
|
510
|
+
const globalSettings = readSettings2(globalPath);
|
|
348
511
|
const globalInstalled = hookExists(globalSettings);
|
|
512
|
+
const globalConfig = getConfig2(globalSettings);
|
|
349
513
|
console.log(globalInstalled ? c.green("\u2713") : c.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", c.dim(`(${globalPath})`));
|
|
514
|
+
if (globalInstalled) {
|
|
515
|
+
console.log(c.dim(` List: ${globalConfig.taskListId || "(all)"}, Keywords: ${globalConfig.keywords?.join(", ") || "dev"}`));
|
|
516
|
+
}
|
|
350
517
|
const cwd = process.cwd();
|
|
351
518
|
const projectPath = getSettingsPath(cwd);
|
|
352
|
-
if (
|
|
353
|
-
const projectSettings =
|
|
519
|
+
if (existsSync2(projectPath)) {
|
|
520
|
+
const projectSettings = readSettings2(projectPath);
|
|
354
521
|
const projectInstalled = hookExists(projectSettings);
|
|
522
|
+
const projectConfig = getConfig2(projectSettings);
|
|
355
523
|
console.log(projectInstalled ? c.green("\u2713") : c.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", c.dim(`(${projectPath})`));
|
|
524
|
+
if (projectInstalled) {
|
|
525
|
+
console.log(c.dim(` List: ${projectConfig.taskListId || "(all)"}, Keywords: ${projectConfig.keywords?.join(", ") || "dev"}`));
|
|
526
|
+
}
|
|
356
527
|
} else {
|
|
357
|
-
console.log(c.dim("\xB7"), "Project:", c.dim("
|
|
528
|
+
console.log(c.dim("\xB7"), "Project:", c.dim("No .claude/settings.json"));
|
|
529
|
+
}
|
|
530
|
+
const lists = getAvailableTaskLists();
|
|
531
|
+
if (lists.length > 0) {
|
|
532
|
+
console.log();
|
|
533
|
+
console.log(c.bold("Available task lists:"));
|
|
534
|
+
lists.forEach((list) => console.log(c.dim(` - ${list}`)));
|
|
535
|
+
}
|
|
536
|
+
const envTaskList = process.env.CLAUDE_CODE_TASK_LIST_ID;
|
|
537
|
+
if (envTaskList) {
|
|
538
|
+
console.log();
|
|
539
|
+
console.log(c.bold("Environment (legacy):"));
|
|
540
|
+
console.log(` CLAUDE_CODE_TASK_LIST_ID: ${envTaskList}`);
|
|
358
541
|
}
|
|
359
|
-
console.log();
|
|
360
|
-
console.log(c.bold("Environment:"));
|
|
361
|
-
const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
|
|
362
|
-
const keywords = process.env.CHECK_TASKS_KEYWORDS;
|
|
363
|
-
const disabled = process.env.CHECK_TASKS_DISABLED === "1";
|
|
364
|
-
console.log(" CLAUDE_CODE_TASK_LIST_ID:", taskListId || c.yellow("(not set)"));
|
|
365
|
-
console.log(" CHECK_TASKS_KEYWORDS:", keywords || c.dim("dev (default)"));
|
|
366
|
-
console.log(" CHECK_TASKS_DISABLED:", disabled ? c.yellow("yes") : "no");
|
|
367
542
|
console.log();
|
|
368
543
|
}
|
|
369
544
|
var args = process.argv.slice(2);
|
|
@@ -373,6 +548,9 @@ switch (command) {
|
|
|
373
548
|
case "install":
|
|
374
549
|
install(commandArgs);
|
|
375
550
|
break;
|
|
551
|
+
case "config":
|
|
552
|
+
configure(commandArgs);
|
|
553
|
+
break;
|
|
376
554
|
case "uninstall":
|
|
377
555
|
uninstall(commandArgs);
|
|
378
556
|
break;
|
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,30 @@ function getSessionName(transcriptPath) {
|
|
|
54
79
|
return null;
|
|
55
80
|
}
|
|
56
81
|
}
|
|
82
|
+
function getTaskLists() {
|
|
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 getTasksFromList(listId) {
|
|
93
|
+
const tasksDir = join(homedir(), ".claude", "tasks", listId);
|
|
94
|
+
if (!existsSync(tasksDir))
|
|
95
|
+
return [];
|
|
96
|
+
try {
|
|
97
|
+
const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
|
|
98
|
+
return taskFiles.map((file) => {
|
|
99
|
+
const content = readFileSync(join(tasksDir, file), "utf-8");
|
|
100
|
+
return JSON.parse(content);
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
57
106
|
function approve() {
|
|
58
107
|
console.log(JSON.stringify({ decision: "approve" }));
|
|
59
108
|
process.exit(0);
|
|
@@ -63,51 +112,65 @@ function block(reason) {
|
|
|
63
112
|
process.exit(0);
|
|
64
113
|
}
|
|
65
114
|
function run() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!taskListId) {
|
|
115
|
+
const hookInput = readStdinJson();
|
|
116
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
117
|
+
const config = getConfig(cwd);
|
|
118
|
+
if (config.enabled === false) {
|
|
71
119
|
approve();
|
|
72
120
|
}
|
|
73
|
-
const hookInput = readStdinJson();
|
|
74
121
|
let sessionName = null;
|
|
75
122
|
if (hookInput?.transcript_path) {
|
|
76
123
|
sessionName = getSessionName(hookInput.transcript_path);
|
|
77
124
|
}
|
|
78
|
-
const nameToCheck = sessionName || taskListId || "";
|
|
79
|
-
const keywords =
|
|
80
|
-
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
|
|
81
|
-
if (!matchesKeyword) {
|
|
125
|
+
const nameToCheck = sessionName || config.taskListId || "";
|
|
126
|
+
const keywords = config.keywords || ["dev"];
|
|
127
|
+
const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
|
|
128
|
+
if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
|
|
82
129
|
approve();
|
|
83
130
|
}
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
131
|
+
let listsToCheck = [];
|
|
132
|
+
if (config.taskListId) {
|
|
133
|
+
listsToCheck = [config.taskListId];
|
|
134
|
+
} else {
|
|
135
|
+
const allLists = getTaskLists();
|
|
136
|
+
if (keywords.length > 0) {
|
|
137
|
+
listsToCheck = allLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
|
|
138
|
+
} else {
|
|
139
|
+
listsToCheck = allLists;
|
|
140
|
+
}
|
|
87
141
|
}
|
|
88
|
-
|
|
89
|
-
if (taskFiles.length === 0) {
|
|
142
|
+
if (listsToCheck.length === 0) {
|
|
90
143
|
approve();
|
|
91
144
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
145
|
+
let allPending = [];
|
|
146
|
+
let allInProgress = [];
|
|
147
|
+
let allCompleted = [];
|
|
148
|
+
let activeListId = null;
|
|
149
|
+
for (const listId of listsToCheck) {
|
|
150
|
+
const tasks = getTasksFromList(listId);
|
|
151
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
152
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
153
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
154
|
+
if (pending.length > 0 || inProgress.length > 0) {
|
|
155
|
+
activeListId = listId;
|
|
156
|
+
}
|
|
157
|
+
allPending.push(...pending);
|
|
158
|
+
allInProgress.push(...inProgress);
|
|
159
|
+
allCompleted.push(...completed);
|
|
160
|
+
}
|
|
161
|
+
const remainingCount = allPending.length + allInProgress.length;
|
|
100
162
|
if (remainingCount > 0) {
|
|
101
|
-
const nextTasks =
|
|
163
|
+
const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
|
|
102
164
|
`);
|
|
165
|
+
const listInfo = activeListId ? ` in "${activeListId}"` : "";
|
|
103
166
|
const prompt = `
|
|
104
|
-
STOP BLOCKED: You have ${remainingCount} tasks remaining (${
|
|
167
|
+
STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
|
|
105
168
|
|
|
106
169
|
You MUST continue working. Do NOT stop until all tasks are completed.
|
|
107
170
|
|
|
108
171
|
Next pending tasks:
|
|
109
172
|
${nextTasks}
|
|
110
|
-
${
|
|
173
|
+
${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
|
|
111
174
|
|
|
112
175
|
INSTRUCTIONS:
|
|
113
176
|
1. Use TaskList to see all tasks
|