@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.
Files changed (4) hide show
  1. package/README.md +97 -89
  2. package/dist/cli.js +110 -430
  3. package/dist/hook.js +26 -105
  4. 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
- This hook intercepts Claude's "Stop" event and:
7
+ When you have a task list configured (`CLAUDE_CODE_TASK_LIST_ID`), this hook:
8
8
 
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
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
- ### 1. Install the CLI globally
18
+ ### Prerequisites
16
19
 
17
- ```bash
18
- bun add -g @hasnaxyz/hook-checktasks
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
- ### 2. Install the hook
23
+ ### Quick Install
24
24
 
25
25
  ```bash
26
- # Auto-detect (git repo project, else → prompt)
27
- hook-checktasks install
26
+ # Install globally (applies to all Claude Code sessions)
27
+ bunx @hasnaxyz/hook-checktasks install --global
28
28
 
29
- # Install globally
30
- hook-checktasks install --global
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
- 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")
39
-
40
- ## Configuration
41
-
42
- ### Update configuration
33
+ ### Check Installation Status
43
34
 
44
35
  ```bash
45
- hook-checktasks config # Current project
46
- hook-checktasks config --global # Global settings
36
+ bunx @hasnaxyz/hook-checktasks status
47
37
  ```
48
38
 
49
- ### Check status
39
+ ### Uninstall
50
40
 
51
41
  ```bash
52
- hook-checktasks status
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
- Shows:
56
- - Where hook is installed (global/project)
57
- - Current configuration
58
- - Available task lists
49
+ ## Configuration
59
50
 
60
- ### Uninstall
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
- hook-checktasks uninstall # Current project
64
- hook-checktasks uninstall --global # Global
65
- hook-checktasks uninstall /path # Specific path
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 Configuration Works
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
- Configuration is stored in `.claude/settings.json`:
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": [{ "hooks": [{ "type": "command", "command": "bunx @hasnaxyz/hook-checktasks run" }] }]
76
- },
77
- "checkTasksConfig": {
78
- "taskListId": "myproject-dev",
79
- "keywords": ["dev"],
80
- "enabled": true
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
- ### Config Options
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
- Still supported for backwards compatibility:
106
+ Tasks are stored as JSON files in `~/.claude/tasks/{taskListId}/`:
102
107
 
103
- ```bash
104
- CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
105
- CHECK_TASKS_KEYWORDS=dev,sprint claude
106
- CHECK_TASKS_DISABLED=1 claude
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 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)
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 Apply to global settings
120
- /path/to/repo Apply to specific project
130
+ --global, -g Apply to global settings (~/.claude/settings.json)
121
131
  ```
122
132
 
123
- ## How it Works
133
+ ## Development
124
134
 
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
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
- const hookInput = readStdinJson();
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 || config.taskListId || "";
144
- const keywords = config.keywords || ["dev"];
145
- const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
146
- if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
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
- let listsToCheck = [];
150
- if (config.taskListId) {
151
- listsToCheck = [config.taskListId];
152
- } else {
153
- const projectLists = getProjectTaskLists(cwd);
154
- if (projectLists.length > 0) {
155
- if (keywords.length > 0) {
156
- listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
157
- } else {
158
- listsToCheck = projectLists;
159
- }
160
- }
161
- }
162
- if (listsToCheck.length === 0) {
89
+ const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
90
+ if (!existsSync(tasksDir)) {
163
91
  approve();
164
92
  }
165
- let allPending = [];
166
- let allInProgress = [];
167
- let allCompleted = [];
168
- let activeListId = null;
169
- for (const listId of listsToCheck) {
170
- const tasks = getTasksFromList(listId);
171
- const pending = tasks.filter((t) => t.status === "pending");
172
- const inProgress = tasks.filter((t) => t.status === "in_progress");
173
- const completed = tasks.filter((t) => t.status === "completed");
174
- if (pending.length > 0 || inProgress.length > 0) {
175
- activeListId = listId;
176
- }
177
- allPending.push(...pending);
178
- allInProgress.push(...inProgress);
179
- allCompleted.push(...completed);
93
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
94
+ if (taskFiles.length === 0) {
95
+ approve();
180
96
  }
181
- const remainingCount = allPending.length + allInProgress.length;
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 = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
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${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
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
- ${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
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, readdirSync as readdirSync2 } from "fs";
216
- import { join as join2, dirname, resolve } from "path";
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 CONFIG_KEY2 = "checkTasksConfig";
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
- ${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
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
- ${c.bold("USAGE:")}
234
- hook-checktasks install [path] Install the hook
235
- hook-checktasks config [path] Update configuration
236
- hook-checktasks uninstall [path] Remove the hook
237
- hook-checktasks status Show hook status
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
- ${c.bold("OPTIONS:")}
241
- ${c.dim("(no args)")} Auto-detect: if in git repo \u2192 install there, else \u2192 prompt
242
- --global, -g Apply to ~/.claude/settings.json
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
- ${c.bold("EXAMPLES:")}
249
- hook-checktasks install ${c.dim("# Install with config prompts")}
250
- hook-checktasks install --global ${c.dim("# Global install")}
251
- hook-checktasks install -t myproject-dev -y ${c.dim("# Non-interactive")}
252
- hook-checktasks config ${c.dim("# Update task list ID, keywords")}
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
- ${c.bold("GLOBAL CLI INSTALL:")}
256
- bun add -g ${PACKAGE_NAME}
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 isGitRepo(path) {
260
- return existsSync2(join2(path, ".git"));
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(targetPath, ".claude", "settings.json");
180
+ return join2(process.cwd(), ".claude", "settings.json");
267
181
  }
268
- function readSettings2(path) {
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}@latest run`;
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
- async function promptForConfig(existingConfig = {}, projectPath) {
440
- const config = { ...existingConfig };
441
- const availableLists = projectPath ? getProjectTaskLists2(projectPath) : getAllTaskLists2();
245
+ function install(global) {
246
+ const scope = global ? "global" : "project";
247
+ const settingsPath = getSettingsPath(global);
442
248
  console.log(`
443
- ${c.bold("Configuration")}
249
+ ${colors.bold("Installing hook-checktasks")} (${scope})
444
250
  `);
445
- console.log(c.bold("Task List ID:"));
446
- if (availableLists.length > 0) {
447
- console.log(c.dim(" Available lists for this project:"));
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
- if (!target)
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(c.yellow("!"), `Hook already installed in ${target.label}`);
505
- if (!options.yes) {
506
- const update = await prompt("Update configuration? (y/n): ");
507
- if (update.toLowerCase() !== "y")
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
- let settings = readSettings2(settingsPath);
550
- if (!hookExists(settings)) {
551
- console.log(c.red("\u2717"), `Hook not installed in ${target.label}`);
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(c.green("\u2713"), `Configuration updated`);
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(c.bold("New configuration:"));
564
- console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
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
- async function uninstall(args) {
278
+ function uninstall(global) {
279
+ const scope = global ? "global" : "project";
280
+ const settingsPath = getSettingsPath(global);
569
281
  console.log(`
570
- ${c.bold("hook-checktasks uninstall")}
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(c.yellow("!"), `No settings file at ${settingsPath}`);
285
+ console.log(colors.yellow("!"), "No settings file found at", settingsPath);
578
286
  return;
579
287
  }
580
- const settings = readSettings2(settingsPath);
288
+ const settings = readSettings(settingsPath);
581
289
  if (!hookExists(settings)) {
582
- console.log(c.yellow("!"), `Hook not found in ${target.label}`);
290
+ console.log(colors.yellow("!"), "Hook not found in", settingsPath);
583
291
  return;
584
292
  }
585
- const updated = removeHook(settings);
586
- writeSettings(settingsPath, updated);
587
- console.log(c.green("\u2713"), `Removed from ${target.label}`);
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
- ${c.bold("hook-checktasks status")}
300
+ ${colors.bold("hook-checktasks status")}
592
301
  `);
593
- const globalPath = getSettingsPath("global");
594
- const globalSettings = readSettings2(globalPath);
302
+ const globalPath = getSettingsPath(true);
303
+ const projectPath = getSettingsPath(false);
304
+ const globalSettings = readSettings(globalPath);
595
305
  const globalInstalled = hookExists(globalSettings);
596
- const globalConfig = getConfig2(globalSettings);
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 = readSettings2(projectPath);
308
+ const projectSettings = readSettings(projectPath);
605
309
  const projectInstalled = hookExists(projectSettings);
606
- const projectConfig = getConfig2(projectSettings);
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(c.dim("\xB7"), "Project:", c.dim("No .claude/settings.json"));
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 commandArgs = args.slice(1);
323
+ var isGlobal = args.includes("--global") || args.includes("-g");
641
324
  switch (command) {
642
325
  case "install":
643
- install(commandArgs);
644
- break;
645
- case "config":
646
- configure(commandArgs);
326
+ install(isGlobal);
647
327
  break;
648
328
  case "uninstall":
649
- uninstall(commandArgs);
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(c.red(`Unknown command: ${command}`));
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
- const hookInput = readStdinJson();
130
- const cwd = hookInput?.cwd || process.cwd();
131
- const config = getConfig(cwd);
132
- if (config.enabled === false) {
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 || config.taskListId || "";
140
- const keywords = config.keywords || ["dev"];
141
- const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
142
- if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
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
- let listsToCheck = [];
146
- if (config.taskListId) {
147
- listsToCheck = [config.taskListId];
148
- } else {
149
- const projectLists = getProjectTaskLists(cwd);
150
- if (projectLists.length > 0) {
151
- if (keywords.length > 0) {
152
- listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
153
- } else {
154
- listsToCheck = projectLists;
155
- }
156
- }
157
- }
158
- if (listsToCheck.length === 0) {
84
+ const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
85
+ if (!existsSync(tasksDir)) {
159
86
  approve();
160
87
  }
161
- let allPending = [];
162
- let allInProgress = [];
163
- let allCompleted = [];
164
- let activeListId = null;
165
- for (const listId of listsToCheck) {
166
- const tasks = getTasksFromList(listId);
167
- const pending = tasks.filter((t) => t.status === "pending");
168
- const inProgress = tasks.filter((t) => t.status === "in_progress");
169
- const completed = tasks.filter((t) => t.status === "completed");
170
- if (pending.length > 0 || inProgress.length > 0) {
171
- activeListId = listId;
172
- }
173
- allPending.push(...pending);
174
- allInProgress.push(...inProgress);
175
- allCompleted.push(...completed);
88
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
89
+ if (taskFiles.length === 0) {
90
+ approve();
176
91
  }
177
- const remainingCount = allPending.length + allInProgress.length;
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 = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
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${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
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
- ${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasnaxyz/hook-checktasks",
3
- "version": "0.2.5",
3
+ "version": "1.0.0",
4
4
  "description": "Claude Code hook that prevents stopping when there are pending tasks",
5
5
  "type": "module",
6
6
  "bin": {