@hasnaxyz/hook-checktasks 1.0.0 → 1.0.2

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