@mauribadnights/clooks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mauribadnights
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # clooks
2
+
3
+ Persistent hook runtime for Claude Code — eliminate cold starts, get observability.
4
+
5
+ ## The Problem
6
+
7
+ Claude Code spawns a fresh process for every hook invocation. Power users with multiple hooks (safety guards, context injectors, custom scripts) accumulate **100+ process spawns per session**. Each Node.js cold start costs 50-100ms. That's 6-11 seconds of pure overhead per session — and you get zero visibility into what your hooks are doing.
8
+
9
+ ## How clooks Fixes It
10
+
11
+ One persistent HTTP server handles all your hooks. Claude Code's [built-in HTTP hook support](https://docs.anthropic.com/en/docs/claude-code/hooks) POSTs to `localhost:7890` instead of spawning processes. **One process instead of hundreds.**
12
+
13
+ ```
14
+ ┌─────────────┐ POST /hooks/PreToolUse ┌──────────────────┐
15
+ │ │ ──────────────────────────────► │ │
16
+ │ Claude Code │ POST /hooks/Stop │ clooks daemon │
17
+ │ │ ──────────────────────────────► │ (persistent) │
18
+ │ │ POST /hooks/... │ │
19
+ │ │ ──────────────────────────────► │ ┌────────────┐ │
20
+ │ │ │ │ handler A │ │
21
+ │ │ ◄────────────── JSON ───────── │ │ handler B │ │
22
+ │ │ │ │ handler C │ │
23
+ └─────────────┘ │ └────────────┘ │
24
+ │ metrics.jsonl │
25
+ └──────────────────┘
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ npm install -g clooks
32
+
33
+ # If you have existing hooks in settings.json:
34
+ clooks migrate # converts command hooks → HTTP hooks + manifest
35
+
36
+ # Or start fresh:
37
+ clooks init # creates ~/.clooks/manifest.yaml
38
+
39
+ clooks start # starts the daemon
40
+ ```
41
+
42
+ That's it. Claude Code will now POST to your daemon instead of spawning processes.
43
+
44
+ ## Commands
45
+
46
+ | Command | Description |
47
+ |---|---|
48
+ | `clooks start` | Start the daemon (background by default, `--foreground` for debug) |
49
+ | `clooks stop` | Stop the daemon |
50
+ | `clooks status` | Show daemon status, uptime, and handler count |
51
+ | `clooks stats` | Show hook execution metrics (fires, errors, latency) |
52
+ | `clooks migrate` | Convert `settings.json` command hooks to HTTP hooks |
53
+ | `clooks restore` | Restore original `settings.json` from backup |
54
+ | `clooks doctor` | Run diagnostic health checks |
55
+ | `clooks init` | Create default config directory and example manifest |
56
+ | `clooks ensure-running` | Start daemon if not running (used by SessionStart hook) |
57
+
58
+ ## Manifest Format
59
+
60
+ Handlers are defined in `~/.clooks/manifest.yaml`:
61
+
62
+ ```yaml
63
+ handlers:
64
+ PreToolUse:
65
+ - id: safety-guard
66
+ type: script
67
+ command: node ~/hooks/guard.js
68
+ timeout: 3000
69
+ enabled: true
70
+
71
+ - id: context-injector
72
+ type: inline
73
+ module: ~/hooks/context.js
74
+ timeout: 2000
75
+
76
+ Stop:
77
+ - id: session-logger
78
+ type: script
79
+ command: ~/hooks/log-session.sh
80
+
81
+ settings:
82
+ port: 7890
83
+ logLevel: info
84
+ ```
85
+
86
+ **Handler types:**
87
+ - `script` — runs a shell command, pipes hook JSON to stdin, reads JSON from stdout
88
+ - `inline` — imports a JS module and calls its default export (faster, no subprocess)
89
+
90
+ ## Stats
91
+
92
+ ```
93
+ $ clooks stats
94
+
95
+ Event Fires Errors Avg (ms) Min (ms) Max (ms)
96
+ ------------------------------------------------------------------------
97
+ PreToolUse 47 0 1.2 0.8 3.1
98
+ Stop 12 0 2.4 1.1 5.6
99
+ UserPromptSubmit 12 1 1.8 0.9 4.2
100
+
101
+ Total fires: 71 | Total errors: 1 | Spawns saved: ~71
102
+ ```
103
+
104
+ ## How It Works with Claude Code
105
+
106
+ After `clooks migrate`, your `settings.json` looks like this:
107
+
108
+ ```json
109
+ {
110
+ "hooks": {
111
+ "SessionStart": [{ "hooks": [
112
+ { "type": "command", "command": "clooks ensure-running" }
113
+ ]}],
114
+ "PreToolUse": [{ "hooks": [
115
+ { "type": "http", "url": "http://localhost:7890/hooks/PreToolUse" }
116
+ ]}]
117
+ }
118
+ }
119
+ ```
120
+
121
+ The `SessionStart` command hook ensures the daemon is running (fast no-op if already up). All other hooks are HTTP POSTs — no process spawning, no cold starts.
122
+
123
+ Handlers that fail 3 times consecutively are auto-disabled to prevent cascading failures.
124
+
125
+ ## Configuration
126
+
127
+ | Item | Default |
128
+ |---|---|
129
+ | Port | `7890` |
130
+ | Config directory | `~/.clooks/` |
131
+ | Manifest | `~/.clooks/manifest.yaml` |
132
+ | Metrics | `~/.clooks/metrics.jsonl` |
133
+ | Daemon log | `~/.clooks/daemon.log` |
134
+ | PID file | `~/.clooks/daemon.pid` |
135
+
136
+ ## Comparison
137
+
138
+ | | Without clooks | With clooks |
139
+ |---|---|---|
140
+ | **Process model** | New process per hook invocation | One persistent HTTP server |
141
+ | **Cold start** | 50-100ms per invocation | 0ms (already running) |
142
+ | **State** | Stateless — each invocation starts fresh | Persistent — share state across invocations |
143
+ | **Observability** | None | Metrics, stats, logs, doctor diagnostics |
144
+ | **Failure handling** | Silent | Auto-disable after 3 consecutive failures |
145
+
146
+ ## License
147
+
148
+ MIT
149
+
150
+ ## Roadmap
151
+
152
+ - **v0.2:** Matcher support in manifest, LLM call batching, token cost tracking
153
+ - **v0.3:** Plugin ecosystem, dependency resolution between handlers
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // clooks CLI entry point
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ const commander_1 = require("commander");
6
+ const manifest_js_1 = require("./manifest.js");
7
+ const metrics_js_1 = require("./metrics.js");
8
+ const server_js_1 = require("./server.js");
9
+ const migrate_js_1 = require("./migrate.js");
10
+ const doctor_js_1 = require("./doctor.js");
11
+ const constants_js_1 = require("./constants.js");
12
+ const fs_1 = require("fs");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('clooks')
16
+ .description('Persistent hook runtime for Claude Code')
17
+ .version('0.1.0');
18
+ // --- start ---
19
+ program
20
+ .command('start')
21
+ .description('Start the clooks daemon')
22
+ .option('-f, --foreground', 'Run in foreground (default: background/detached)')
23
+ .action(async (opts) => {
24
+ if (!opts.foreground) {
25
+ // Background mode: check if already running, then spawn detached
26
+ if ((0, server_js_1.isDaemonRunning)()) {
27
+ console.log('Daemon is already running.');
28
+ process.exit(0);
29
+ }
30
+ // Ensure config dir exists
31
+ if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
32
+ (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
33
+ }
34
+ console.log('Starting clooks daemon in background...');
35
+ (0, server_js_1.startDaemonBackground)();
36
+ // Give it a moment to start
37
+ await new Promise((r) => setTimeout(r, 500));
38
+ if ((0, server_js_1.isDaemonRunning)()) {
39
+ const pid = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
40
+ console.log(`Daemon started (pid ${pid}), listening on 127.0.0.1:${constants_js_1.DEFAULT_PORT}`);
41
+ }
42
+ else {
43
+ console.log('Daemon started. Check ~/.clooks/daemon.log if issues arise.');
44
+ }
45
+ process.exit(0);
46
+ }
47
+ // Foreground mode: run the actual server
48
+ try {
49
+ const manifest = (0, manifest_js_1.loadManifest)();
50
+ const metrics = new metrics_js_1.MetricsCollector();
51
+ const port = manifest.settings?.port ?? constants_js_1.DEFAULT_PORT;
52
+ const handlerCount = Object.values(manifest.handlers)
53
+ .reduce((sum, arr) => sum + (arr?.length ?? 0), 0);
54
+ await (0, server_js_1.startDaemon)(manifest, metrics);
55
+ console.log(`clooks daemon running on 127.0.0.1:${port} (${handlerCount} handler${handlerCount !== 1 ? 's' : ''})`);
56
+ }
57
+ catch (err) {
58
+ console.error('Failed to start daemon:', err instanceof Error ? err.message : err);
59
+ process.exit(1);
60
+ }
61
+ });
62
+ // --- stop ---
63
+ program
64
+ .command('stop')
65
+ .description('Stop the clooks daemon')
66
+ .action(() => {
67
+ if ((0, server_js_1.stopDaemon)()) {
68
+ console.log('Daemon stopped.');
69
+ }
70
+ else {
71
+ console.log('Daemon is not running (no PID file or process not found).');
72
+ }
73
+ });
74
+ // --- status ---
75
+ program
76
+ .command('status')
77
+ .description('Show daemon status')
78
+ .action(async () => {
79
+ const running = (0, server_js_1.isDaemonRunning)();
80
+ if (!running) {
81
+ console.log('Status: stopped');
82
+ return;
83
+ }
84
+ const pid = (0, fs_1.existsSync)(constants_js_1.PID_FILE) ? (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim() : '?';
85
+ // Try to hit health endpoint
86
+ try {
87
+ const { get } = await import('http');
88
+ const data = await new Promise((resolve, reject) => {
89
+ const req = get(`http://127.0.0.1:${constants_js_1.DEFAULT_PORT}/health`, (res) => {
90
+ let body = '';
91
+ res.on('data', (chunk) => { body += chunk.toString(); });
92
+ res.on('end', () => resolve(body));
93
+ });
94
+ req.on('error', reject);
95
+ req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
96
+ });
97
+ const health = JSON.parse(data);
98
+ console.log(`Status: running`);
99
+ console.log(`PID: ${pid}`);
100
+ console.log(`Port: ${health.port}`);
101
+ console.log(`Uptime: ${formatUptime(health.uptime)}`);
102
+ console.log(`Handlers loaded: ${health.handlers_loaded}`);
103
+ }
104
+ catch {
105
+ console.log(`Status: running (pid ${pid})`);
106
+ console.log(`Note: Could not reach health endpoint on port ${constants_js_1.DEFAULT_PORT}`);
107
+ }
108
+ });
109
+ // --- stats ---
110
+ program
111
+ .command('stats')
112
+ .description('Show hook execution metrics')
113
+ .action(() => {
114
+ const metrics = new metrics_js_1.MetricsCollector();
115
+ console.log(metrics.formatStatsTable());
116
+ });
117
+ // --- migrate ---
118
+ program
119
+ .command('migrate')
120
+ .description('Migrate Claude Code settings.json to use clooks HTTP hooks')
121
+ .action(() => {
122
+ try {
123
+ const result = (0, migrate_js_1.migrate)();
124
+ console.log('Migration complete!');
125
+ console.log(` Settings: ${result.settingsPath}`);
126
+ console.log(` Manifest: ${result.manifestPath}`);
127
+ console.log(` Handlers created: ${result.handlersCreated}`);
128
+ console.log(` Backup: ~/.clooks/settings.backup.json`);
129
+ console.log('\nRun "clooks start" to start the daemon.');
130
+ }
131
+ catch (err) {
132
+ console.error('Migration failed:', err instanceof Error ? err.message : err);
133
+ process.exit(1);
134
+ }
135
+ });
136
+ // --- restore ---
137
+ program
138
+ .command('restore')
139
+ .description('Restore original settings.json from backup')
140
+ .action(() => {
141
+ try {
142
+ const path = (0, migrate_js_1.restore)();
143
+ console.log(`Restored settings to: ${path}`);
144
+ }
145
+ catch (err) {
146
+ console.error('Restore failed:', err instanceof Error ? err.message : err);
147
+ process.exit(1);
148
+ }
149
+ });
150
+ // --- doctor ---
151
+ program
152
+ .command('doctor')
153
+ .description('Run diagnostic health checks')
154
+ .action(async () => {
155
+ const results = await (0, doctor_js_1.runDoctor)();
156
+ const icons = { ok: '[OK]', warn: '[WARN]', error: '[ERR]' };
157
+ for (const r of results) {
158
+ console.log(` ${icons[r.status]} ${r.check}: ${r.message}`);
159
+ }
160
+ const errors = results.filter((r) => r.status === 'error').length;
161
+ const warns = results.filter((r) => r.status === 'warn').length;
162
+ console.log(`\n${results.length} checks: ${results.length - errors - warns} ok, ${warns} warnings, ${errors} errors`);
163
+ if (errors > 0)
164
+ process.exit(1);
165
+ });
166
+ // --- ensure-running ---
167
+ program
168
+ .command('ensure-running')
169
+ .description('Start daemon if not already running (used by SessionStart hook)')
170
+ .action(async () => {
171
+ if ((0, server_js_1.isDaemonRunning)()) {
172
+ // Already running — exit silently and fast
173
+ process.exit(0);
174
+ }
175
+ // Ensure config dir exists
176
+ if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
177
+ (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
178
+ }
179
+ // If no manifest, create a default one
180
+ const { MANIFEST_PATH } = await import('./constants.js');
181
+ if (!(0, fs_1.existsSync)(MANIFEST_PATH)) {
182
+ (0, manifest_js_1.createDefaultManifest)();
183
+ }
184
+ (0, server_js_1.startDaemonBackground)();
185
+ process.exit(0);
186
+ });
187
+ // --- init ---
188
+ program
189
+ .command('init')
190
+ .description('Create default config directory and example manifest')
191
+ .action(() => {
192
+ if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
193
+ (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
194
+ }
195
+ const path = (0, manifest_js_1.createDefaultManifest)();
196
+ console.log(`Created: ${path}`);
197
+ console.log('Edit this file to configure your hook handlers.');
198
+ });
199
+ program.parse();
200
+ function formatUptime(seconds) {
201
+ if (seconds < 60)
202
+ return `${seconds}s`;
203
+ if (seconds < 3600)
204
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
205
+ const h = Math.floor(seconds / 3600);
206
+ const m = Math.floor((seconds % 3600) / 60);
207
+ return `${h}h ${m}m`;
208
+ }
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_PORT = 7890;
2
+ export declare const CONFIG_DIR: string;
3
+ export declare const MANIFEST_PATH: string;
4
+ export declare const PID_FILE: string;
5
+ export declare const METRICS_FILE: string;
6
+ export declare const LOG_FILE: string;
7
+ export declare const SETTINGS_BACKUP: string;
8
+ export declare const MAX_CONSECUTIVE_FAILURES = 3;
9
+ export declare const DEFAULT_HANDLER_TIMEOUT = 5000;
10
+ export declare const HOOK_EVENTS: string[];
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // clooks constants
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.HOOK_EVENTS = exports.DEFAULT_HANDLER_TIMEOUT = exports.MAX_CONSECUTIVE_FAILURES = exports.SETTINGS_BACKUP = exports.LOG_FILE = exports.METRICS_FILE = exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = void 0;
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ exports.DEFAULT_PORT = 7890;
8
+ exports.CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.clooks');
9
+ exports.MANIFEST_PATH = (0, path_1.join)(exports.CONFIG_DIR, 'manifest.yaml');
10
+ exports.PID_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'daemon.pid');
11
+ exports.METRICS_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'metrics.jsonl');
12
+ exports.LOG_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'daemon.log');
13
+ exports.SETTINGS_BACKUP = (0, path_1.join)(exports.CONFIG_DIR, 'settings.backup.json');
14
+ exports.MAX_CONSECUTIVE_FAILURES = 3;
15
+ exports.DEFAULT_HANDLER_TIMEOUT = 5000; // ms
16
+ exports.HOOK_EVENTS = [
17
+ 'SessionStart',
18
+ 'UserPromptSubmit',
19
+ 'PreToolUse',
20
+ 'PostToolUse',
21
+ 'Stop',
22
+ 'SubagentStart',
23
+ 'SubagentStop',
24
+ 'Notification',
25
+ 'ConfigChange',
26
+ ];
@@ -0,0 +1,5 @@
1
+ import type { DiagnosticResult } from './types.js';
2
+ /**
3
+ * Run all diagnostic checks and return results.
4
+ */
5
+ export declare function runDoctor(): Promise<DiagnosticResult[]>;
package/dist/doctor.js ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ // clooks doctor — diagnostics and health checks
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.runDoctor = runDoctor;
5
+ const fs_1 = require("fs");
6
+ const http_1 = require("http");
7
+ const child_process_1 = require("child_process");
8
+ const path_1 = require("path");
9
+ const os_1 = require("os");
10
+ const constants_js_1 = require("./constants.js");
11
+ const manifest_js_1 = require("./manifest.js");
12
+ const server_js_1 = require("./server.js");
13
+ /**
14
+ * Run all diagnostic checks and return results.
15
+ */
16
+ async function runDoctor() {
17
+ const results = [];
18
+ // 1. Config directory exists
19
+ results.push(checkConfigDir());
20
+ // 2. Manifest exists and is valid
21
+ results.push(checkManifest());
22
+ // 3. Daemon is running (PID file + process alive)
23
+ results.push(checkDaemonRunning());
24
+ // 4. Port is reachable
25
+ results.push(await checkPortReachable());
26
+ // 5. Script handler commands are executable
27
+ results.push(...checkHandlerCommands());
28
+ // 6. Settings.json has HTTP hooks pointing to clooks
29
+ results.push(checkSettingsHooks());
30
+ // 7. No stale PID file
31
+ results.push(checkStalePid());
32
+ return results;
33
+ }
34
+ function checkConfigDir() {
35
+ if ((0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
36
+ return { check: 'Config directory', status: 'ok', message: `${constants_js_1.CONFIG_DIR} exists` };
37
+ }
38
+ return { check: 'Config directory', status: 'error', message: `${constants_js_1.CONFIG_DIR} does not exist. Run "clooks start" to create it.` };
39
+ }
40
+ function checkManifest() {
41
+ if (!(0, fs_1.existsSync)(constants_js_1.MANIFEST_PATH)) {
42
+ return { check: 'Manifest', status: 'warn', message: 'manifest.yaml not found. No handlers configured.' };
43
+ }
44
+ try {
45
+ (0, manifest_js_1.loadManifest)();
46
+ return { check: 'Manifest', status: 'ok', message: 'manifest.yaml is valid' };
47
+ }
48
+ catch (err) {
49
+ return { check: 'Manifest', status: 'error', message: `manifest.yaml is invalid: ${err instanceof Error ? err.message : String(err)}` };
50
+ }
51
+ }
52
+ function checkDaemonRunning() {
53
+ if ((0, server_js_1.isDaemonRunning)()) {
54
+ const pid = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
55
+ return { check: 'Daemon process', status: 'ok', message: `Running (pid ${pid})` };
56
+ }
57
+ return { check: 'Daemon process', status: 'warn', message: 'Daemon is not running' };
58
+ }
59
+ function checkPortReachable() {
60
+ return new Promise((resolve) => {
61
+ const req = (0, http_1.get)(`http://127.0.0.1:${constants_js_1.DEFAULT_PORT}/health`, (res) => {
62
+ let data = '';
63
+ res.on('data', (chunk) => {
64
+ data += chunk.toString();
65
+ });
66
+ res.on('end', () => {
67
+ try {
68
+ const parsed = JSON.parse(data);
69
+ if (parsed.status === 'ok') {
70
+ resolve({ check: 'Port reachable', status: 'ok', message: `127.0.0.1:${constants_js_1.DEFAULT_PORT} responds with status ok` });
71
+ }
72
+ else {
73
+ resolve({ check: 'Port reachable', status: 'warn', message: `Port responds but status is: ${parsed.status}` });
74
+ }
75
+ }
76
+ catch {
77
+ resolve({ check: 'Port reachable', status: 'warn', message: 'Port responds but returned invalid JSON' });
78
+ }
79
+ });
80
+ });
81
+ req.on('error', () => {
82
+ resolve({ check: 'Port reachable', status: 'error', message: `Cannot reach 127.0.0.1:${constants_js_1.DEFAULT_PORT}` });
83
+ });
84
+ req.setTimeout(3000, () => {
85
+ req.destroy();
86
+ resolve({ check: 'Port reachable', status: 'error', message: 'Health check timed out' });
87
+ });
88
+ });
89
+ }
90
+ function checkHandlerCommands() {
91
+ const results = [];
92
+ try {
93
+ const manifest = (0, manifest_js_1.loadManifest)();
94
+ for (const [_event, handlers] of Object.entries(manifest.handlers)) {
95
+ for (const handler of handlers) {
96
+ if (handler.type !== 'script' || !handler.command)
97
+ continue;
98
+ // Extract the base command (first word)
99
+ const baseCmd = handler.command.split(/\s+/)[0];
100
+ // Skip built-in shell commands
101
+ if (['echo', 'cat', 'true', 'false', 'test', '['].includes(baseCmd)) {
102
+ results.push({ check: `Handler "${handler.id}"`, status: 'ok', message: `Command: ${baseCmd} (shell builtin)` });
103
+ continue;
104
+ }
105
+ try {
106
+ (0, child_process_1.execSync)(`which ${baseCmd}`, { stdio: 'pipe' });
107
+ results.push({ check: `Handler "${handler.id}"`, status: 'ok', message: `Command "${baseCmd}" found in PATH` });
108
+ }
109
+ catch {
110
+ results.push({ check: `Handler "${handler.id}"`, status: 'error', message: `Command "${baseCmd}" not found in PATH` });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // If manifest can't be loaded, skip handler checks (already caught by checkManifest)
117
+ }
118
+ if (results.length === 0) {
119
+ results.push({ check: 'Handler commands', status: 'ok', message: 'No script handlers to check' });
120
+ }
121
+ return results;
122
+ }
123
+ function checkSettingsHooks() {
124
+ const candidates = [
125
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', 'settings.local.json'),
126
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', 'settings.json'),
127
+ ];
128
+ for (const path of candidates) {
129
+ if (!(0, fs_1.existsSync)(path))
130
+ continue;
131
+ try {
132
+ const raw = (0, fs_1.readFileSync)(path, 'utf-8');
133
+ const settings = JSON.parse(raw);
134
+ if (!settings.hooks) {
135
+ return { check: 'Settings hooks', status: 'warn', message: 'No hooks configured in ' + path };
136
+ }
137
+ // settings.hooks[event] is an array of rule groups, each with a hooks[] array
138
+ const hasHttpHook = Object.values(settings.hooks).some((ruleGroups) => ruleGroups.some((rule) => rule.hooks?.some((e) => e.type === 'http' && e.url?.includes(`localhost:${constants_js_1.DEFAULT_PORT}`))));
139
+ if (hasHttpHook) {
140
+ return { check: 'Settings hooks', status: 'ok', message: `HTTP hooks point to clooks in ${path}` };
141
+ }
142
+ return { check: 'Settings hooks', status: 'warn', message: `No HTTP hooks pointing to clooks in ${path}. Run "clooks migrate".` };
143
+ }
144
+ catch {
145
+ return { check: 'Settings hooks', status: 'error', message: `Failed to parse ${path}` };
146
+ }
147
+ }
148
+ return { check: 'Settings hooks', status: 'warn', message: 'No Claude Code settings.json found' };
149
+ }
150
+ function checkStalePid() {
151
+ if (!(0, fs_1.existsSync)(constants_js_1.PID_FILE)) {
152
+ return { check: 'Stale PID', status: 'ok', message: 'No PID file' };
153
+ }
154
+ const pidStr = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
155
+ const pid = parseInt(pidStr, 10);
156
+ if (isNaN(pid)) {
157
+ return { check: 'Stale PID', status: 'error', message: 'PID file contains invalid value' };
158
+ }
159
+ try {
160
+ process.kill(pid, 0);
161
+ return { check: 'Stale PID', status: 'ok', message: `PID ${pid} is alive` };
162
+ }
163
+ catch {
164
+ return { check: 'Stale PID', status: 'error', message: `Stale PID file: process ${pid} is dead. Remove ${constants_js_1.PID_FILE} or run "clooks start".` };
165
+ }
166
+ }
@@ -0,0 +1,19 @@
1
+ import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput } from './types.js';
2
+ /** Reset all handler states (useful for testing) */
3
+ export declare function resetHandlerStates(): void;
4
+ /** Get a copy of the handler states map */
5
+ export declare function getHandlerStates(): Map<string, HandlerState>;
6
+ /**
7
+ * Execute all handlers for an event in parallel.
8
+ * Returns merged results array.
9
+ */
10
+ export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[]): Promise<HandlerResult[]>;
11
+ /**
12
+ * Execute a script handler: spawn a child process, pipe input JSON to stdin,
13
+ * read stdout as JSON response.
14
+ */
15
+ export declare function executeScriptHandler(handler: HandlerConfig, input: HookInput): Promise<HandlerResult>;
16
+ /**
17
+ * Execute an inline handler: dynamically import a JS module and call its default export.
18
+ */
19
+ export declare function executeInlineHandler(handler: HandlerConfig, input: HookInput): Promise<HandlerResult>;