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