@hasnaxyz/hook-checktasks 0.1.2 → 0.2.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 +259 -58
- 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,30 @@ async function prompt(question) {
|
|
|
252
325
|
});
|
|
253
326
|
});
|
|
254
327
|
}
|
|
328
|
+
function getAllTaskLists() {
|
|
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
|
+
}
|
|
338
|
+
function getProjectTaskLists(projectPath) {
|
|
339
|
+
const allLists = getAllTaskLists();
|
|
340
|
+
const dirName = projectPath.split("/").filter(Boolean).pop() || "";
|
|
341
|
+
const projectLists = allLists.filter((list) => {
|
|
342
|
+
const listLower = list.toLowerCase();
|
|
343
|
+
const dirLower = dirName.toLowerCase();
|
|
344
|
+
if (listLower.startsWith(dirLower + "-"))
|
|
345
|
+
return true;
|
|
346
|
+
if (listLower.includes(dirLower))
|
|
347
|
+
return true;
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
return projectLists;
|
|
351
|
+
}
|
|
255
352
|
async function resolveTarget(args) {
|
|
256
353
|
if (args.includes("--global") || args.includes("-g")) {
|
|
257
354
|
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
@@ -266,8 +363,7 @@ async function resolveTarget(args) {
|
|
|
266
363
|
return { path: fullPath, label: `project (${fullPath})` };
|
|
267
364
|
}
|
|
268
365
|
const cwd = process.cwd();
|
|
269
|
-
|
|
270
|
-
if (isRepo) {
|
|
366
|
+
if (isGitRepo(cwd)) {
|
|
271
367
|
console.log(c.green("\u2713"), `Detected git repo: ${c.cyan(cwd)}`);
|
|
272
368
|
return { path: cwd, label: `project (${cwd})` };
|
|
273
369
|
}
|
|
@@ -302,6 +398,45 @@ async function resolveTarget(args) {
|
|
|
302
398
|
return null;
|
|
303
399
|
}
|
|
304
400
|
}
|
|
401
|
+
async function promptForConfig(existingConfig = {}, projectPath) {
|
|
402
|
+
const config = { ...existingConfig };
|
|
403
|
+
const availableLists = projectPath ? getProjectTaskLists(projectPath) : getAllTaskLists();
|
|
404
|
+
console.log(`
|
|
405
|
+
${c.bold("Configuration")}
|
|
406
|
+
`);
|
|
407
|
+
console.log(c.bold("Task List ID:"));
|
|
408
|
+
if (availableLists.length > 0) {
|
|
409
|
+
console.log(c.dim(" Available lists for this project:"));
|
|
410
|
+
availableLists.forEach((list, i) => {
|
|
411
|
+
console.log(c.dim(` ${i + 1}. ${list}`));
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
console.log(c.dim(" No task lists found for this project"));
|
|
415
|
+
}
|
|
416
|
+
console.log(c.dim(` Leave empty to check all matching lists
|
|
417
|
+
`));
|
|
418
|
+
const currentList = config.taskListId || "(all lists)";
|
|
419
|
+
const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
|
|
420
|
+
if (listInput) {
|
|
421
|
+
const num = parseInt(listInput, 10);
|
|
422
|
+
if (!isNaN(num) && num > 0 && num <= availableLists.length) {
|
|
423
|
+
config.taskListId = availableLists[num - 1];
|
|
424
|
+
} else {
|
|
425
|
+
config.taskListId = listInput;
|
|
426
|
+
}
|
|
427
|
+
} else if (!existingConfig.taskListId) {
|
|
428
|
+
config.taskListId = undefined;
|
|
429
|
+
}
|
|
430
|
+
const currentKeywords = config.keywords?.join(", ") || "dev";
|
|
431
|
+
const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
|
|
432
|
+
if (keywordsInput) {
|
|
433
|
+
config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
434
|
+
} else if (!existingConfig.keywords) {
|
|
435
|
+
config.keywords = ["dev"];
|
|
436
|
+
}
|
|
437
|
+
config.enabled = true;
|
|
438
|
+
return config;
|
|
439
|
+
}
|
|
305
440
|
async function install(args) {
|
|
306
441
|
console.log(`
|
|
307
442
|
${c.bold("hook-checktasks install")}
|
|
@@ -310,18 +445,59 @@ ${c.bold("hook-checktasks install")}
|
|
|
310
445
|
if (!target)
|
|
311
446
|
return;
|
|
312
447
|
const settingsPath = getSettingsPath(target.path);
|
|
313
|
-
|
|
448
|
+
let settings = readSettings2(settingsPath);
|
|
314
449
|
if (hookExists(settings)) {
|
|
315
|
-
console.log(c.yellow("!"), `
|
|
316
|
-
|
|
450
|
+
console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
|
|
451
|
+
const update = await prompt("Update configuration? (y/n): ");
|
|
452
|
+
if (update.toLowerCase() !== "y")
|
|
453
|
+
return;
|
|
454
|
+
} else {
|
|
455
|
+
settings = addHook(settings);
|
|
317
456
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
console.log(
|
|
457
|
+
const existingConfig = getConfig2(settings);
|
|
458
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
459
|
+
const config = await promptForConfig(existingConfig, projectPath);
|
|
460
|
+
settings = setConfig(settings, config);
|
|
461
|
+
writeSettings(settingsPath, settings);
|
|
462
|
+
console.log();
|
|
463
|
+
console.log(c.green("\u2713"), `Installed to ${target.label}`);
|
|
464
|
+
console.log();
|
|
465
|
+
console.log(c.bold("Configuration:"));
|
|
466
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
467
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
468
|
+
console.log();
|
|
469
|
+
}
|
|
470
|
+
async function configure(args) {
|
|
471
|
+
console.log(`
|
|
472
|
+
${c.bold("hook-checktasks config")}
|
|
324
473
|
`);
|
|
474
|
+
const target = await resolveTarget(args);
|
|
475
|
+
if (!target)
|
|
476
|
+
return;
|
|
477
|
+
const settingsPath = getSettingsPath(target.path);
|
|
478
|
+
if (!existsSync2(settingsPath)) {
|
|
479
|
+
console.log(c.red("\u2717"), `No settings file at ${settingsPath}`);
|
|
480
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
let settings = readSettings2(settingsPath);
|
|
484
|
+
if (!hookExists(settings)) {
|
|
485
|
+
console.log(c.red("\u2717"), `Hook not installed in ${target.label}`);
|
|
486
|
+
console.log(c.dim(" Run 'hook-checktasks install' first"));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const existingConfig = getConfig2(settings);
|
|
490
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
491
|
+
const config = await promptForConfig(existingConfig, projectPath);
|
|
492
|
+
settings = setConfig(settings, config);
|
|
493
|
+
writeSettings(settingsPath, settings);
|
|
494
|
+
console.log();
|
|
495
|
+
console.log(c.green("\u2713"), `Configuration updated`);
|
|
496
|
+
console.log();
|
|
497
|
+
console.log(c.bold("New configuration:"));
|
|
498
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
|
|
499
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
500
|
+
console.log();
|
|
325
501
|
}
|
|
326
502
|
async function uninstall(args) {
|
|
327
503
|
console.log(`
|
|
@@ -335,7 +511,7 @@ ${c.bold("hook-checktasks uninstall")}
|
|
|
335
511
|
console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
|
|
336
512
|
return;
|
|
337
513
|
}
|
|
338
|
-
const settings =
|
|
514
|
+
const settings = readSettings2(settingsPath);
|
|
339
515
|
if (!hookExists(settings)) {
|
|
340
516
|
console.log(c.yellow("!"), `Hook not found in ${target.label}`);
|
|
341
517
|
return;
|
|
@@ -349,26 +525,48 @@ function status() {
|
|
|
349
525
|
${c.bold("hook-checktasks status")}
|
|
350
526
|
`);
|
|
351
527
|
const globalPath = getSettingsPath("global");
|
|
352
|
-
const globalSettings =
|
|
528
|
+
const globalSettings = readSettings2(globalPath);
|
|
353
529
|
const globalInstalled = hookExists(globalSettings);
|
|
530
|
+
const globalConfig = getConfig2(globalSettings);
|
|
354
531
|
console.log(globalInstalled ? c.green("\u2713") : c.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", c.dim(`(${globalPath})`));
|
|
532
|
+
if (globalInstalled) {
|
|
533
|
+
console.log(c.dim(` List: ${globalConfig.taskListId || "(all)"}, Keywords: ${globalConfig.keywords?.join(", ") || "dev"}`));
|
|
534
|
+
}
|
|
355
535
|
const cwd = process.cwd();
|
|
356
536
|
const projectPath = getSettingsPath(cwd);
|
|
357
|
-
if (
|
|
358
|
-
const projectSettings =
|
|
537
|
+
if (existsSync2(projectPath)) {
|
|
538
|
+
const projectSettings = readSettings2(projectPath);
|
|
359
539
|
const projectInstalled = hookExists(projectSettings);
|
|
540
|
+
const projectConfig = getConfig2(projectSettings);
|
|
360
541
|
console.log(projectInstalled ? c.green("\u2713") : c.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", c.dim(`(${projectPath})`));
|
|
542
|
+
if (projectInstalled) {
|
|
543
|
+
console.log(c.dim(` List: ${projectConfig.taskListId || "(all)"}, Keywords: ${projectConfig.keywords?.join(", ") || "dev"}`));
|
|
544
|
+
}
|
|
361
545
|
} else {
|
|
362
|
-
console.log(c.dim("\xB7"), "Project:", c.dim("
|
|
546
|
+
console.log(c.dim("\xB7"), "Project:", c.dim("No .claude/settings.json"));
|
|
547
|
+
}
|
|
548
|
+
const projectLists = getProjectTaskLists(cwd);
|
|
549
|
+
if (projectLists.length > 0) {
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(c.bold("Task lists for this project:"));
|
|
552
|
+
projectLists.forEach((list) => console.log(c.dim(` - ${list}`)));
|
|
553
|
+
} else {
|
|
554
|
+
const allLists = getAllTaskLists();
|
|
555
|
+
if (allLists.length > 0) {
|
|
556
|
+
console.log();
|
|
557
|
+
console.log(c.bold("All task lists:"), c.dim("(none match this project)"));
|
|
558
|
+
allLists.slice(0, 10).forEach((list) => console.log(c.dim(` - ${list}`)));
|
|
559
|
+
if (allLists.length > 10) {
|
|
560
|
+
console.log(c.dim(` ... and ${allLists.length - 10} more`));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const envTaskList = process.env.CLAUDE_CODE_TASK_LIST_ID;
|
|
565
|
+
if (envTaskList) {
|
|
566
|
+
console.log();
|
|
567
|
+
console.log(c.bold("Environment (legacy):"));
|
|
568
|
+
console.log(` CLAUDE_CODE_TASK_LIST_ID: ${envTaskList}`);
|
|
363
569
|
}
|
|
364
|
-
console.log();
|
|
365
|
-
console.log(c.bold("Environment:"));
|
|
366
|
-
const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
|
|
367
|
-
const keywords = process.env.CHECK_TASKS_KEYWORDS;
|
|
368
|
-
const disabled = process.env.CHECK_TASKS_DISABLED === "1";
|
|
369
|
-
console.log(" CLAUDE_CODE_TASK_LIST_ID:", taskListId || c.yellow("(not set)"));
|
|
370
|
-
console.log(" CHECK_TASKS_KEYWORDS:", keywords || c.dim("dev (default)"));
|
|
371
|
-
console.log(" CHECK_TASKS_DISABLED:", disabled ? c.yellow("yes") : "no");
|
|
372
570
|
console.log();
|
|
373
571
|
}
|
|
374
572
|
var args = process.argv.slice(2);
|
|
@@ -378,6 +576,9 @@ switch (command) {
|
|
|
378
576
|
case "install":
|
|
379
577
|
install(commandArgs);
|
|
380
578
|
break;
|
|
579
|
+
case "config":
|
|
580
|
+
configure(commandArgs);
|
|
581
|
+
break;
|
|
381
582
|
case "uninstall":
|
|
382
583
|
uninstall(commandArgs);
|
|
383
584
|
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
|