@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 +215 -68
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +27 -0
- package/dist/cli.js +23 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +10 -1
- package/dist/doctor.js +57 -1
- package/dist/filter.d.ts +11 -0
- package/dist/filter.js +42 -0
- package/dist/handlers.d.ts +8 -2
- package/dist/handlers.js +112 -35
- package/dist/index.d.ts +8 -3
- package/dist/index.js +22 -1
- package/dist/llm.d.ts +19 -0
- package/dist/llm.js +225 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +38 -9
- package/dist/metrics.d.ts +29 -2
- package/dist/metrics.js +135 -6
- package/dist/migrate.js +6 -2
- package/dist/prefetch.d.ts +11 -0
- package/dist/prefetch.js +71 -0
- package/dist/server.d.ts +8 -2
- package/dist/server.js +79 -10
- package/dist/types.d.ts +78 -16
- package/dist/watcher.d.ts +18 -0
- package/dist/watcher.js +120 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,50 +1,61 @@
|
|
|
1
1
|
# clooks
|
|
2
2
|
|
|
3
|
-
Persistent hook runtime for Claude Code
|
|
3
|
+
**Persistent hook runtime for Claude Code.** Eliminate cold starts. Get observability.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@mauribadnights/clooks)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Performance
|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
16
|
+
> Benchmarked on Apple Silicon (M-series), Node v24.4.1. Run `npm run bench` to reproduce.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
#
|
|
34
|
-
clooks migrate
|
|
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
|
-
#
|
|
37
|
-
clooks init
|
|
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
|
-
|
|
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`
|
|
88
|
-
- `inline`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
**Setup:**
|
|
107
176
|
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
182
|
+
**Prompt template variables:**
|
|
122
183
|
|
|
123
|
-
|
|
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
|
-
|
|
194
|
+
**LLM handler options:**
|
|
126
195
|
|
|
127
|
-
|
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
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
|
-
|
|
206
|
+
**How batching works:**
|
|
137
207
|
|
|
138
|
-
|
|
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
|
-
|
|
210
|
+
### Intelligent Filtering
|
|
147
211
|
|
|
148
|
-
|
|
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
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.
|
|
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
|
|
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();
|
package/dist/constants.d.ts
CHANGED
|
@@ -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'
|
|
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
|
+
}
|
package/dist/filter.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/handlers.d.ts
CHANGED
|
@@ -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.
|