@mauribadnights/clooks 0.1.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,61 @@
1
1
  # clooks
2
2
 
3
- Persistent hook runtime for Claude Code eliminate cold starts, get observability.
3
+ **Persistent hook runtime for Claude Code.** Eliminate cold starts. Get observability.
4
4
 
5
- ## The Problem
5
+ [![npm version](https://img.shields.io/npm/v/@mauribadnights/clooks.svg)](https://www.npmjs.com/package/@mauribadnights/clooks)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
7
 
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
+ ## Performance
8
9
 
9
- ## How clooks Fixes It
10
+ | Metric | Without clooks | With clooks | Improvement |
11
+ |--------|---------------|-------------|-------------|
12
+ | Single hook invocation | ~34.6ms | ~0.31ms | **112x faster** |
13
+ | Full session (120 invocations) | ~3,986ms | ~23ms | **99% time saved** |
14
+ | 5 parallel handlers | ~424ms | ~96ms | **4.4x faster** |
10
15
 
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.**
16
+ > Benchmarked on Apple Silicon (M-series), Node v24.4.1. Run `npm run bench` to reproduce.
12
17
 
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
- ```
18
+ ## The Problem
19
+
20
+ Claude Code spawns a fresh process for every hook invocation. Each Node.js cold start costs 30-40ms. Power users with multiple hooks accumulate 100+ process spawns per session -- that is 4-6 seconds of pure overhead, with zero visibility into what your hooks are doing or how they fail.
27
21
 
28
22
  ## Quick Start
29
23
 
30
24
  ```bash
31
- npm install -g clooks
25
+ npm install -g @mauribadnights/clooks
32
26
 
33
- # If you have existing hooks in settings.json:
34
- clooks migrate # converts command hooks HTTP hooks + manifest
27
+ # Option A: Migrate existing hooks automatically
28
+ clooks migrate # converts command hooks to HTTP hooks + manifest
29
+ clooks start # starts the daemon
35
30
 
36
- # Or start fresh:
37
- clooks init # creates ~/.clooks/manifest.yaml
31
+ # Option B: Start fresh
32
+ clooks init # creates ~/.clooks/manifest.yaml
33
+ clooks start
34
+ ```
35
+
36
+ That is it. Claude Code will now POST to your daemon instead of spawning processes.
37
+
38
+ ## How It Works
38
39
 
39
- clooks start # starts the daemon
40
40
  ```
41
+ Claude Code clooks daemon (localhost:7890)
42
+ | |
43
+ |-- SessionStart ------> POST /hooks/SessionStart ------> [handler1, handler2]
44
+ |-- UserPromptSubmit --> POST /hooks/UserPromptSubmit --> [handler3]
45
+ |-- PreToolUse (x50) --> POST /hooks/PreToolUse --------> [handler4, handler5]
46
+ |-- Stop --------------> POST /hooks/Stop ---------------> [handler6]
47
+ | |
48
+ |<-------------- JSON responses ---------|
49
+ ```
50
+
51
+ One persistent process. Zero cold starts. Full observability.
41
52
 
42
- That's it. Claude Code will now POST to your daemon instead of spawning processes.
53
+ After `clooks migrate`, your `settings.json` is rewritten so that `SessionStart` runs a single command hook (`clooks ensure-running`) and all other hooks become HTTP POSTs. The daemon loads handlers from `~/.clooks/manifest.yaml` and dispatches them in parallel per event. Handlers that fail 3 times consecutively are auto-disabled to prevent cascading failures.
43
54
 
44
55
  ## Commands
45
56
 
46
57
  | Command | Description |
47
- |---|---|
58
+ |---------|-------------|
48
59
  | `clooks start` | Start the daemon (background by default, `--foreground` for debug) |
49
60
  | `clooks stop` | Stop the daemon |
50
61
  | `clooks status` | Show daemon status, uptime, and handler count |
@@ -53,7 +64,7 @@ That's it. Claude Code will now POST to your daemon instead of spawning processe
53
64
  | `clooks restore` | Restore original `settings.json` from backup |
54
65
  | `clooks doctor` | Run diagnostic health checks |
55
66
  | `clooks init` | Create default config directory and example manifest |
56
- | `clooks ensure-running` | Start daemon if not running (used by SessionStart hook) |
67
+ | `clooks ensure-running` | Start daemon if not already running (used by SessionStart hook) |
57
68
 
58
69
  ## Manifest Format
59
70
 
@@ -63,13 +74,13 @@ Handlers are defined in `~/.clooks/manifest.yaml`:
63
74
  handlers:
64
75
  PreToolUse:
65
76
  - id: safety-guard
66
- type: script
77
+ type: script # runs a shell command
67
78
  command: node ~/hooks/guard.js
68
79
  timeout: 3000
69
80
  enabled: true
70
81
 
71
82
  - id: context-injector
72
- type: inline
83
+ type: inline # imports a JS module directly (no subprocess)
73
84
  module: ~/hooks/context.js
74
85
  timeout: 2000
75
86
 
@@ -84,10 +95,13 @@ settings:
84
95
  ```
85
96
 
86
97
  **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)
98
+ - `script` -- runs a shell command, pipes hook JSON to stdin, reads JSON from stdout.
99
+ - `inline` -- imports a JS module and calls its default export. Faster; no subprocess overhead.
100
+ - `llm` -- calls Anthropic Messages API. Supports prompt templates, batching, and cost tracking. *(v0.2+)*
101
+
102
+ ## Observability
89
103
 
90
- ## Stats
104
+ ### Execution Metrics
91
105
 
92
106
  ```
93
107
  $ clooks stats
@@ -101,53 +115,186 @@ UserPromptSubmit 12 1 1.8 0.9 4.2
101
115
  Total fires: 71 | Total errors: 1 | Spawns saved: ~71
102
116
  ```
103
117
 
104
- ## How It Works with Claude Code
118
+ ### Diagnostics
119
+
120
+ ```
121
+ $ clooks doctor
122
+
123
+ [pass] Daemon is running (PID 44721, uptime 2h 13m)
124
+ [pass] Port 7890 is responding
125
+ [pass] Manifest loaded: 4 handlers across 3 events
126
+ [pass] settings.json has HTTP hooks pointing to clooks
127
+ [pass] No handlers in circuit-breaker state
128
+ [warn] 1 handler error in last 24h (session-logger on Stop)
129
+ ```
130
+
131
+ ## Comparison
132
+
133
+ | | Without clooks | With clooks |
134
+ |---|---|---|
135
+ | **Process model** | New process per hook invocation | One persistent HTTP server |
136
+ | **Cold start overhead** | 30-40ms per invocation | 0ms (already running) |
137
+ | **State management** | Stateless -- each invocation starts fresh | Persistent -- share state across invocations |
138
+ | **Observability** | None | Metrics, stats, logs, doctor diagnostics |
139
+ | **Error handling** | Silent failures | Auto-disable after 3 consecutive failures |
140
+
141
+ ## Configuration Reference
142
+
143
+ | Option | Default | Description |
144
+ |--------|---------|-------------|
145
+ | Port | `7890` | HTTP server port |
146
+ | Config directory | `~/.clooks/` | Root configuration directory |
147
+ | Manifest | `~/.clooks/manifest.yaml` | Handler definitions |
148
+ | Metrics | `~/.clooks/metrics.jsonl` | Execution metrics log |
149
+ | Daemon log | `~/.clooks/daemon.log` | Server output log |
150
+ | PID file | `~/.clooks/daemon.pid` | Process ID file |
151
+
152
+ ## v0.2 Features
153
+
154
+ ### LLM Handlers
155
+
156
+ Call the Anthropic Messages API directly from your manifest. Handlers with the same `batchGroup` are combined into a single API call, saving tokens and latency.
157
+
158
+ ```yaml
159
+ handlers:
160
+ PreToolUse:
161
+ - id: code-review
162
+ type: llm
163
+ model: claude-haiku-4-5
164
+ prompt: "Review this tool call for $TOOL_NAME with args: $ARGUMENTS"
165
+ batchGroup: analysis
166
+ timeout: 15000
167
+
168
+ - id: security-check
169
+ type: llm
170
+ model: claude-haiku-4-5
171
+ prompt: "Check for security issues in $TOOL_NAME call: $ARGUMENTS"
172
+ batchGroup: analysis # batched with code-review into one API call
173
+ ```
105
174
 
106
- After `clooks migrate`, your `settings.json` looks like this:
175
+ **Setup:**
107
176
 
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
- }
177
+ ```bash
178
+ npm install @anthropic-ai/sdk # peer dependency, only needed for llm handlers
179
+ export ANTHROPIC_API_KEY=sk-... # or set in manifest: settings.anthropicApiKey
119
180
  ```
120
181
 
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.
182
+ **Prompt template variables:**
122
183
 
123
- Handlers that fail 3 times consecutively are auto-disabled to prevent cascading failures.
184
+ | Variable | Source | Description |
185
+ |----------|--------|-------------|
186
+ | `$TRANSCRIPT` | Pre-fetched transcript file | Last 50KB of session transcript |
187
+ | `$GIT_STATUS` | `git status --porcelain` | Current working tree status |
188
+ | `$GIT_DIFF` | `git diff --stat` | Changed files summary (max 20KB) |
189
+ | `$ARGUMENTS` | `hook_input.tool_input` | JSON-stringified tool arguments |
190
+ | `$TOOL_NAME` | `hook_input.tool_name` | Name of the tool being called |
191
+ | `$PROMPT` | `hook_input.prompt` | User's prompt (UserPromptSubmit only) |
192
+ | `$CWD` | `hook_input.cwd` | Current working directory |
124
193
 
125
- ## Configuration
194
+ **LLM handler options:**
126
195
 
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` |
196
+ | Field | Type | Default | Description |
197
+ |-------|------|---------|-------------|
198
+ | `model` | string | required | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6` |
199
+ | `prompt` | string | required | Prompt template with `$VARIABLE` interpolation |
200
+ | `batchGroup` | string | optional | Group ID -- handlers with same group make one API call |
201
+ | `maxTokens` | number | `1024` | Maximum output tokens |
202
+ | `temperature` | number | `1.0` | Sampling temperature |
203
+ | `filter` | string | optional | Keyword filter (see Filtering) |
204
+ | `timeout` | number | `30000` | Timeout in milliseconds |
135
205
 
136
- ## Comparison
206
+ **How batching works:**
137
207
 
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 |
208
+ When multiple LLM handlers share a `batchGroup` on the same event, clooks combines their prompts into a single multi-task API call and splits the structured response back to each handler. This means 3 Haiku calls become 1, saving ~2/3 of the input token cost and eliminating 2 round-trips.
145
209
 
146
- ## License
210
+ ### Intelligent Filtering
147
211
 
148
- MIT
212
+ Skip handlers based on keywords. The `filter` field works on **all handler types** -- script, inline, and llm.
213
+
214
+ **Filter syntax:**
215
+
216
+ ```
217
+ filter: "word1|word2" # run if input contains word1 OR word2
218
+ filter: "!word" # run unless input contains word
219
+ filter: "word1|!word2" # run if word1 present AND word2 absent
220
+ ```
221
+
222
+ Matching is case-insensitive against the full JSON-serialized hook input.
223
+
224
+ ```yaml
225
+ handlers:
226
+ PreToolUse:
227
+ - id: bash-guard
228
+ type: script
229
+ command: node ~/hooks/guard.js
230
+ filter: "Bash|Execute|!Read" # runs for Bash/Execute, never for Read
231
+ ```
232
+
233
+ ### Shared Context Pre-fetch
234
+
235
+ Fetch transcript, git status, or git diff once per hook event and share across all handlers. Avoids redundant I/O when multiple handlers need the same data. Use `$VARIABLE` interpolation in LLM prompts.
236
+
237
+ ```yaml
238
+ prefetch:
239
+ - transcript
240
+ - git_status
241
+ - git_diff
242
+
243
+ handlers:
244
+ Stop:
245
+ - id: session-summary
246
+ type: llm
247
+ model: claude-haiku-4-5
248
+ prompt: "Summarize this session:\n$TRANSCRIPT\n\nGit changes:\n$GIT_DIFF"
249
+ ```
250
+
251
+ **Available prefetch keys:**
252
+
253
+ | Key | Source | Max size | Description |
254
+ |-----|--------|----------|-------------|
255
+ | `transcript` | `transcript_path` file | 50KB (tail) | Session conversation history |
256
+ | `git_status` | `git status --porcelain` | unbounded | Working tree status |
257
+ | `git_diff` | `git diff --stat` | 20KB | Changed files summary |
258
+
259
+ Pre-fetched data is cached for the duration of a single event dispatch. Errors on individual keys are silently caught -- a failed `git_status` won't prevent `transcript` from loading.
260
+
261
+ ### Cost Tracking
262
+
263
+ Track LLM token usage and costs per handler and model.
264
+
265
+ ```
266
+ $ clooks costs
267
+
268
+ LLM Cost Summary
269
+ Total: $0.0142 (4,280 tokens)
270
+
271
+ By Model:
272
+ claude-haiku-4-5 $0.0142 (4,280 tokens)
273
+
274
+ By Handler:
275
+ code-review $0.0089 (12 calls, avg 178 tokens)
276
+ security-check $0.0053 (12 calls, avg 178 tokens)
277
+ ```
278
+
279
+ - Costs are persisted to `~/.clooks/costs.jsonl`
280
+ - Built-in pricing (per million tokens): Haiku ($0.80 / $4.00), Sonnet ($3.00 / $15.00), Opus ($15.00 / $75.00)
281
+ - Batching savings are estimated based on shared input tokens
282
+ - Cost data also appears in `clooks stats` when LLM handlers have been used
149
283
 
150
284
  ## Roadmap
151
285
 
152
- - **v0.2:** Matcher support in manifest, LLM call batching, token cost tracking
153
286
  - **v0.3:** Plugin ecosystem, dependency resolution between handlers
287
+ - **v0.4:** Visual dashboard for hook management and metrics
288
+
289
+ ## Contributing
290
+
291
+ Issues and pull requests are welcome. Run the test suite before submitting:
292
+
293
+ ```bash
294
+ npm test
295
+ npm run bench
296
+ ```
297
+
298
+ ## License
299
+
300
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Generate a random auth token (32 hex chars). */
2
+ export declare function generateAuthToken(): string;
3
+ /** Validate an auth token from request headers. */
4
+ export declare function validateAuth(authHeader: string | undefined, expectedToken: string): boolean;
package/dist/auth.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ // clooks auth — token-based request authentication
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.generateAuthToken = generateAuthToken;
5
+ exports.validateAuth = validateAuth;
6
+ const crypto_1 = require("crypto");
7
+ /** Generate a random auth token (32 hex chars). */
8
+ function generateAuthToken() {
9
+ return (0, crypto_1.randomBytes)(16).toString('hex');
10
+ }
11
+ /** Validate an auth token from request headers. */
12
+ function validateAuth(authHeader, expectedToken) {
13
+ if (!expectedToken)
14
+ return true; // No token configured = no auth required
15
+ if (!authHeader)
16
+ return false;
17
+ // Support "Bearer <token>" format
18
+ const token = authHeader.startsWith('Bearer ')
19
+ ? authHeader.slice(7)
20
+ : authHeader;
21
+ // Constant-time comparison to prevent timing attacks
22
+ if (token.length !== expectedToken.length)
23
+ return false;
24
+ const bufA = Buffer.from(token);
25
+ const bufB = Buffer.from(expectedToken);
26
+ return (0, crypto_1.timingSafeEqual)(bufA, bufB);
27
+ }
package/dist/cli.js CHANGED
@@ -8,19 +8,22 @@ const metrics_js_1 = require("./metrics.js");
8
8
  const server_js_1 = require("./server.js");
9
9
  const migrate_js_1 = require("./migrate.js");
10
10
  const doctor_js_1 = require("./doctor.js");
11
+ const auth_js_1 = require("./auth.js");
11
12
  const constants_js_1 = require("./constants.js");
12
13
  const fs_1 = require("fs");
13
14
  const program = new commander_1.Command();
14
15
  program
15
16
  .name('clooks')
16
17
  .description('Persistent hook runtime for Claude Code')
17
- .version('0.1.0');
18
+ .version('0.2.2');
18
19
  // --- start ---
19
20
  program
20
21
  .command('start')
21
22
  .description('Start the clooks daemon')
22
23
  .option('-f, --foreground', 'Run in foreground (default: background/detached)')
24
+ .option('--no-watch', 'Disable file watching for manifest changes')
23
25
  .action(async (opts) => {
26
+ const noWatch = opts.watch === false;
24
27
  if (!opts.foreground) {
25
28
  // Background mode: check if already running, then spawn detached
26
29
  if ((0, server_js_1.isDaemonRunning)()) {
@@ -32,7 +35,7 @@ program
32
35
  (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
33
36
  }
34
37
  console.log('Starting clooks daemon in background...');
35
- (0, server_js_1.startDaemonBackground)();
38
+ (0, server_js_1.startDaemonBackground)({ noWatch });
36
39
  // Give it a moment to start
37
40
  await new Promise((r) => setTimeout(r, 500));
38
41
  if ((0, server_js_1.isDaemonRunning)()) {
@@ -51,7 +54,7 @@ program
51
54
  const port = manifest.settings?.port ?? constants_js_1.DEFAULT_PORT;
52
55
  const handlerCount = Object.values(manifest.handlers)
53
56
  .reduce((sum, arr) => sum + (arr?.length ?? 0), 0);
54
- await (0, server_js_1.startDaemon)(manifest, metrics);
57
+ await (0, server_js_1.startDaemon)(manifest, metrics, { noWatch });
55
58
  console.log(`clooks daemon running on 127.0.0.1:${port} (${handlerCount} handler${handlerCount !== 1 ? 's' : ''})`);
56
59
  }
57
60
  catch (err) {
@@ -113,6 +116,20 @@ program
113
116
  .action(() => {
114
117
  const metrics = new metrics_js_1.MetricsCollector();
115
118
  console.log(metrics.formatStatsTable());
119
+ // Append cost summary if LLM data exists
120
+ const costStats = metrics.getCostStats();
121
+ if (costStats.totalCost > 0) {
122
+ console.log('');
123
+ console.log(metrics.formatCostTable());
124
+ }
125
+ });
126
+ // --- costs ---
127
+ program
128
+ .command('costs')
129
+ .description('Show LLM cost breakdown')
130
+ .action(() => {
131
+ const metrics = new metrics_js_1.MetricsCollector();
132
+ console.log(metrics.formatCostTable());
116
133
  });
117
134
  // --- migrate ---
118
135
  program
@@ -192,8 +209,10 @@ program
192
209
  if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
193
210
  (0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
194
211
  }
195
- const path = (0, manifest_js_1.createDefaultManifest)();
212
+ const token = (0, auth_js_1.generateAuthToken)();
213
+ const path = (0, manifest_js_1.createDefaultManifest)(token);
196
214
  console.log(`Created: ${path}`);
215
+ console.log(`Auth token: ${token}`);
197
216
  console.log('Edit this file to configure your hook handlers.');
198
217
  });
199
218
  program.parse();
@@ -7,4 +7,12 @@ export declare const LOG_FILE: string;
7
7
  export declare const SETTINGS_BACKUP: string;
8
8
  export declare const MAX_CONSECUTIVE_FAILURES = 3;
9
9
  export declare const DEFAULT_HANDLER_TIMEOUT = 5000;
10
+ export declare const COSTS_FILE: string;
11
+ export declare const DEFAULT_LLM_TIMEOUT = 30000;
12
+ export declare const DEFAULT_LLM_MAX_TOKENS = 1024;
13
+ /** Pricing per million tokens (USD) — as of March 2026 */
14
+ export declare const LLM_PRICING: Record<string, {
15
+ input: number;
16
+ output: number;
17
+ }>;
10
18
  export declare const HOOK_EVENTS: string[];
package/dist/constants.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // clooks constants
3
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;
4
+ exports.HOOK_EVENTS = exports.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = 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
5
  const os_1 = require("os");
6
6
  const path_1 = require("path");
7
7
  exports.DEFAULT_PORT = 7890;
@@ -13,6 +13,15 @@ exports.LOG_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'daemon.log');
13
13
  exports.SETTINGS_BACKUP = (0, path_1.join)(exports.CONFIG_DIR, 'settings.backup.json');
14
14
  exports.MAX_CONSECUTIVE_FAILURES = 3;
15
15
  exports.DEFAULT_HANDLER_TIMEOUT = 5000; // ms
16
+ exports.COSTS_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'costs.jsonl');
17
+ exports.DEFAULT_LLM_TIMEOUT = 30000; // ms
18
+ exports.DEFAULT_LLM_MAX_TOKENS = 1024;
19
+ /** Pricing per million tokens (USD) — as of March 2026 */
20
+ exports.LLM_PRICING = {
21
+ 'claude-haiku-4-5': { input: 0.80, output: 4.00 },
22
+ 'claude-sonnet-4-6': { input: 3.00, output: 15.00 },
23
+ 'claude-opus-4-6': { input: 15.00, output: 75.00 },
24
+ };
16
25
  exports.HOOK_EVENTS = [
17
26
  'SessionStart',
18
27
  'UserPromptSubmit',
package/dist/doctor.js CHANGED
@@ -29,6 +29,8 @@ async function runDoctor() {
29
29
  results.push(checkSettingsHooks());
30
30
  // 7. No stale PID file
31
31
  results.push(checkStalePid());
32
+ // 8. Auth token consistency (if configured)
33
+ results.push(checkAuthToken());
32
34
  return results;
33
35
  }
34
36
  function checkConfigDir() {
@@ -93,7 +95,9 @@ function checkHandlerCommands() {
93
95
  const manifest = (0, manifest_js_1.loadManifest)();
94
96
  for (const [_event, handlers] of Object.entries(manifest.handlers)) {
95
97
  for (const handler of handlers) {
96
- if (handler.type !== 'script' || !handler.command)
98
+ if (handler.type !== 'script')
99
+ continue;
100
+ if (!handler.command)
97
101
  continue;
98
102
  // Extract the base command (first word)
99
103
  const baseCmd = handler.command.split(/\s+/)[0];
@@ -164,3 +168,55 @@ function checkStalePid() {
164
168
  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
169
  }
166
170
  }
171
+ function checkAuthToken() {
172
+ try {
173
+ const manifest = (0, manifest_js_1.loadManifest)();
174
+ const authToken = manifest.settings?.authToken;
175
+ if (!authToken) {
176
+ return { check: 'Auth token', status: 'ok', message: 'No auth token configured (open access)' };
177
+ }
178
+ // Check that settings.json hooks include matching Authorization header
179
+ const candidates = [
180
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', 'settings.local.json'),
181
+ (0, path_1.join)((0, os_1.homedir)(), '.claude', 'settings.json'),
182
+ ];
183
+ for (const path of candidates) {
184
+ if (!(0, fs_1.existsSync)(path))
185
+ continue;
186
+ try {
187
+ const raw = (0, fs_1.readFileSync)(path, 'utf-8');
188
+ const settings = JSON.parse(raw);
189
+ if (!settings.hooks)
190
+ continue;
191
+ const expectedHeader = `Bearer ${authToken}`;
192
+ const httpHooks = [];
193
+ for (const ruleGroups of Object.values(settings.hooks)) {
194
+ for (const rule of ruleGroups) {
195
+ if (!Array.isArray(rule.hooks))
196
+ continue;
197
+ for (const hook of rule.hooks) {
198
+ if (hook.type === 'http' && hook.url?.includes(`localhost:${constants_js_1.DEFAULT_PORT}`)) {
199
+ httpHooks.push(hook);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (httpHooks.length === 0) {
205
+ return { check: 'Auth token', status: 'warn', message: 'Auth token set but no HTTP hooks found in settings.json' };
206
+ }
207
+ const missingAuth = httpHooks.filter(h => h.headers?.['Authorization'] !== expectedHeader);
208
+ if (missingAuth.length > 0) {
209
+ return { check: 'Auth token', status: 'error', message: `Auth token set but ${missingAuth.length} HTTP hook(s) missing matching Authorization header. Run "clooks migrate".` };
210
+ }
211
+ return { check: 'Auth token', status: 'ok', message: 'Auth token matches settings.json hook headers' };
212
+ }
213
+ catch {
214
+ continue;
215
+ }
216
+ }
217
+ return { check: 'Auth token', status: 'warn', message: 'Auth token set but could not verify settings.json headers' };
218
+ }
219
+ catch {
220
+ return { check: 'Auth token', status: 'ok', message: 'Could not load manifest for auth check' };
221
+ }
222
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Evaluate a keyword filter against input text.
3
+ *
4
+ * Filter syntax:
5
+ * "word1|word2|word3" — match if ANY keyword found (OR)
6
+ * "!word" — exclude if keyword found (NOT)
7
+ * Mixed: "word1|word2|!word3" — match word1 OR word2, but NOT if word3 present
8
+ *
9
+ * Returns true if handler should execute, false if filtered out.
10
+ */
11
+ export declare function evaluateFilter(filter: string, input: string): boolean;
package/dist/filter.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ // clooks filter engine — keyword-based handler filtering
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.evaluateFilter = evaluateFilter;
5
+ /**
6
+ * Evaluate a keyword filter against input text.
7
+ *
8
+ * Filter syntax:
9
+ * "word1|word2|word3" — match if ANY keyword found (OR)
10
+ * "!word" — exclude if keyword found (NOT)
11
+ * Mixed: "word1|word2|!word3" — match word1 OR word2, but NOT if word3 present
12
+ *
13
+ * Returns true if handler should execute, false if filtered out.
14
+ */
15
+ function evaluateFilter(filter, input) {
16
+ const terms = filter.split('|').map((t) => t.trim()).filter(Boolean);
17
+ if (terms.length === 0)
18
+ return true;
19
+ const positive = [];
20
+ const negative = [];
21
+ for (const term of terms) {
22
+ if (term.startsWith('!')) {
23
+ negative.push(term.slice(1));
24
+ }
25
+ else {
26
+ positive.push(term);
27
+ }
28
+ }
29
+ const lowerInput = input.toLowerCase();
30
+ // If ANY negative term is found → blocked
31
+ for (const neg of negative) {
32
+ if (lowerInput.includes(neg.toLowerCase())) {
33
+ return false;
34
+ }
35
+ }
36
+ // If there are positive terms, at least ONE must match
37
+ if (positive.length > 0) {
38
+ return positive.some((pos) => lowerInput.includes(pos.toLowerCase()));
39
+ }
40
+ // Only negative terms and none matched → allow
41
+ return true;
42
+ }
@@ -1,13 +1,19 @@
1
- import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput } from './types.js';
1
+ import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput, PrefetchContext } from './types.js';
2
2
  /** Reset all handler states (useful for testing) */
3
3
  export declare function resetHandlerStates(): void;
4
4
  /** Get a copy of the handler states map */
5
5
  export declare function getHandlerStates(): Map<string, HandlerState>;
6
+ /**
7
+ * Reset handler states for handlers that have sessionIsolation: true.
8
+ * Called on SessionStart events.
9
+ */
10
+ export declare function resetSessionIsolatedHandlers(handlers: HandlerConfig[]): void;
6
11
  /**
7
12
  * Execute all handlers for an event in parallel.
8
13
  * Returns merged results array.
14
+ * Optionally accepts pre-fetched context for LLM prompt rendering.
9
15
  */
10
- export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[]): Promise<HandlerResult[]>;
16
+ export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[], context?: PrefetchContext): Promise<HandlerResult[]>;
11
17
  /**
12
18
  * Execute a script handler: spawn a child process, pipe input JSON to stdin,
13
19
  * read stdout as JSON response.