@hasnaxyz/hook-checktasks 0.1.2 → 0.2.0

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