@mauribadnights/clooks 0.1.0 → 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.
- package/README.md +160 -70
- package/dist/cli.js +15 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +10 -1
- package/dist/doctor.js +3 -1
- package/dist/filter.d.ts +11 -0
- package/dist/filter.js +42 -0
- package/dist/handlers.d.ts +3 -2
- package/dist/handlers.js +94 -35
- package/dist/index.d.ts +5 -2
- package/dist/index.js +15 -1
- package/dist/llm.d.ts +19 -0
- package/dist/llm.js +225 -0
- package/dist/manifest.js +29 -4
- package/dist/metrics.d.ts +21 -1
- package/dist/metrics.js +103 -0
- package/dist/prefetch.d.ts +11 -0
- package/dist/prefetch.js +71 -0
- package/dist/server.js +28 -2
- package/dist/types.d.ts +73 -16
- 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
|
|
26
|
+
|
|
27
|
+
# Option A: Migrate existing hooks automatically
|
|
28
|
+
clooks migrate # converts command hooks to HTTP hooks + manifest
|
|
29
|
+
clooks start # starts the daemon
|
|
30
|
+
|
|
31
|
+
# Option B: Start fresh
|
|
32
|
+
clooks init # creates ~/.clooks/manifest.yaml
|
|
33
|
+
clooks start
|
|
34
|
+
```
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
clooks migrate # converts command hooks → HTTP hooks + manifest
|
|
36
|
+
That is it. Claude Code will now POST to your daemon instead of spawning processes.
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
clooks init # creates ~/.clooks/manifest.yaml
|
|
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,12 @@ 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
|
+
|
|
101
|
+
## Observability
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
### Execution Metrics
|
|
91
104
|
|
|
92
105
|
```
|
|
93
106
|
$ clooks stats
|
|
@@ -101,53 +114,130 @@ UserPromptSubmit 12 1 1.8 0.9 4.2
|
|
|
101
114
|
Total fires: 71 | Total errors: 1 | Spawns saved: ~71
|
|
102
115
|
```
|
|
103
116
|
|
|
104
|
-
|
|
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
|
|
117
|
+
### Diagnostics
|
|
126
118
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
```
|
|
120
|
+
$ clooks doctor
|
|
121
|
+
|
|
122
|
+
[pass] Daemon is running (PID 44721, uptime 2h 13m)
|
|
123
|
+
[pass] Port 7890 is responding
|
|
124
|
+
[pass] Manifest loaded: 4 handlers across 3 events
|
|
125
|
+
[pass] settings.json has HTTP hooks pointing to clooks
|
|
126
|
+
[pass] No handlers in circuit-breaker state
|
|
127
|
+
[warn] 1 handler error in last 24h (session-logger on Stop)
|
|
128
|
+
```
|
|
135
129
|
|
|
136
130
|
## Comparison
|
|
137
131
|
|
|
138
|
-
|
|
|
132
|
+
| | Without clooks | With clooks |
|
|
139
133
|
|---|---|---|
|
|
140
134
|
| **Process model** | New process per hook invocation | One persistent HTTP server |
|
|
141
|
-
| **Cold start** |
|
|
142
|
-
| **State** | Stateless
|
|
135
|
+
| **Cold start overhead** | 30-40ms per invocation | 0ms (already running) |
|
|
136
|
+
| **State management** | Stateless -- each invocation starts fresh | Persistent -- share state across invocations |
|
|
143
137
|
| **Observability** | None | Metrics, stats, logs, doctor diagnostics |
|
|
144
|
-
| **
|
|
138
|
+
| **Error handling** | Silent failures | Auto-disable after 3 consecutive failures |
|
|
145
139
|
|
|
146
|
-
##
|
|
140
|
+
## Configuration Reference
|
|
147
141
|
|
|
148
|
-
|
|
142
|
+
| Option | Default | Description |
|
|
143
|
+
|--------|---------|-------------|
|
|
144
|
+
| Port | `7890` | HTTP server port |
|
|
145
|
+
| Config directory | `~/.clooks/` | Root configuration directory |
|
|
146
|
+
| Manifest | `~/.clooks/manifest.yaml` | Handler definitions |
|
|
147
|
+
| Metrics | `~/.clooks/metrics.jsonl` | Execution metrics log |
|
|
148
|
+
| Daemon log | `~/.clooks/daemon.log` | Server output log |
|
|
149
|
+
| PID file | `~/.clooks/daemon.pid` | Process ID file |
|
|
150
|
+
|
|
151
|
+
## v0.2 Features
|
|
152
|
+
|
|
153
|
+
### LLM Handlers
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
handlers:
|
|
159
|
+
PreToolUse:
|
|
160
|
+
- id: code-review
|
|
161
|
+
type: llm
|
|
162
|
+
model: claude-haiku-4-5
|
|
163
|
+
prompt: "Review this tool call for $TOOL_NAME with args: $ARGUMENTS"
|
|
164
|
+
batchGroup: analysis
|
|
165
|
+
timeout: 15000
|
|
166
|
+
|
|
167
|
+
- id: security-check
|
|
168
|
+
type: llm
|
|
169
|
+
model: claude-haiku-4-5
|
|
170
|
+
prompt: "Check for security issues in $TOOL_NAME call: $ARGUMENTS"
|
|
171
|
+
batchGroup: analysis # batched with code-review into one API call
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Requires `@anthropic-ai/sdk` as a peer dependency and `ANTHROPIC_API_KEY` env var.
|
|
175
|
+
|
|
176
|
+
### Intelligent Filtering
|
|
177
|
+
|
|
178
|
+
Skip handlers based on keywords. Supports OR (`|`) and NOT (`!`) operators. Matching is case-insensitive against the full hook input JSON.
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
handlers:
|
|
182
|
+
PreToolUse:
|
|
183
|
+
- id: bash-guard
|
|
184
|
+
type: script
|
|
185
|
+
command: node ~/hooks/guard.js
|
|
186
|
+
filter: "Bash|Execute|!Read" # runs for Bash/Execute, never for Read
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Shared Context Pre-fetch
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
prefetch:
|
|
195
|
+
- transcript
|
|
196
|
+
- git_status
|
|
197
|
+
- git_diff
|
|
198
|
+
|
|
199
|
+
handlers:
|
|
200
|
+
Stop:
|
|
201
|
+
- id: session-summary
|
|
202
|
+
type: llm
|
|
203
|
+
model: claude-haiku-4-5
|
|
204
|
+
prompt: "Summarize this session:\n$TRANSCRIPT\n\nGit changes:\n$GIT_DIFF"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Cost Tracking
|
|
208
|
+
|
|
209
|
+
Track LLM token usage and costs per handler and model. Pricing is built-in for Haiku 4.5, Sonnet 4.6, and Opus 4.6.
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
$ clooks costs
|
|
213
|
+
|
|
214
|
+
LLM Cost Summary
|
|
215
|
+
Total: $0.0142 (4,280 tokens)
|
|
216
|
+
|
|
217
|
+
By Model:
|
|
218
|
+
claude-haiku-4-5 $0.0142 (4,280 tokens)
|
|
219
|
+
|
|
220
|
+
By Handler:
|
|
221
|
+
code-review $0.0089 (12 calls, avg 178 tokens)
|
|
222
|
+
security-check $0.0053 (12 calls, avg 178 tokens)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Cost data also appears in `clooks stats` when LLM handlers have been used.
|
|
149
226
|
|
|
150
227
|
## Roadmap
|
|
151
228
|
|
|
152
|
-
- **v0.2:** Matcher support in manifest, LLM call batching, token cost tracking
|
|
153
229
|
- **v0.3:** Plugin ecosystem, dependency resolution between handlers
|
|
230
|
+
- **v0.4:** Visual dashboard for hook management and metrics
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Issues and pull requests are welcome. Run the test suite before submitting:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npm test
|
|
238
|
+
npm run bench
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const program = new commander_1.Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name('clooks')
|
|
16
16
|
.description('Persistent hook runtime for Claude Code')
|
|
17
|
-
.version('0.
|
|
17
|
+
.version('0.2.0');
|
|
18
18
|
// --- start ---
|
|
19
19
|
program
|
|
20
20
|
.command('start')
|
|
@@ -113,6 +113,20 @@ program
|
|
|
113
113
|
.action(() => {
|
|
114
114
|
const metrics = new metrics_js_1.MetricsCollector();
|
|
115
115
|
console.log(metrics.formatStatsTable());
|
|
116
|
+
// Append cost summary if LLM data exists
|
|
117
|
+
const costStats = metrics.getCostStats();
|
|
118
|
+
if (costStats.totalCost > 0) {
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(metrics.formatCostTable());
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// --- costs ---
|
|
124
|
+
program
|
|
125
|
+
.command('costs')
|
|
126
|
+
.description('Show LLM cost breakdown')
|
|
127
|
+
.action(() => {
|
|
128
|
+
const metrics = new metrics_js_1.MetricsCollector();
|
|
129
|
+
console.log(metrics.formatCostTable());
|
|
116
130
|
});
|
|
117
131
|
// --- migrate ---
|
|
118
132
|
program
|
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
|
@@ -93,7 +93,9 @@ function checkHandlerCommands() {
|
|
|
93
93
|
const manifest = (0, manifest_js_1.loadManifest)();
|
|
94
94
|
for (const [_event, handlers] of Object.entries(manifest.handlers)) {
|
|
95
95
|
for (const handler of handlers) {
|
|
96
|
-
if (handler.type !== 'script'
|
|
96
|
+
if (handler.type !== 'script')
|
|
97
|
+
continue;
|
|
98
|
+
if (!handler.command)
|
|
97
99
|
continue;
|
|
98
100
|
// Extract the base command (first word)
|
|
99
101
|
const baseCmd = handler.command.split(/\s+/)[0];
|
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,4 +1,4 @@
|
|
|
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 */
|
|
@@ -6,8 +6,9 @@ export declare function getHandlerStates(): Map<string, HandlerState>;
|
|
|
6
6
|
/**
|
|
7
7
|
* Execute all handlers for an event in parallel.
|
|
8
8
|
* Returns merged results array.
|
|
9
|
+
* Optionally accepts pre-fetched context for LLM prompt rendering.
|
|
9
10
|
*/
|
|
10
|
-
export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[]): Promise<HandlerResult[]>;
|
|
11
|
+
export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[], context?: PrefetchContext): Promise<HandlerResult[]>;
|
|
11
12
|
/**
|
|
12
13
|
* Execute a script handler: spawn a child process, pipe input JSON to stdin,
|
|
13
14
|
* read stdout as JSON response.
|
package/dist/handlers.js
CHANGED
|
@@ -10,6 +10,8 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const url_1 = require("url");
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const constants_js_1 = require("./constants.js");
|
|
13
|
+
const filter_js_1 = require("./filter.js");
|
|
14
|
+
const llm_js_1 = require("./llm.js");
|
|
13
15
|
/** Runtime state per handler ID */
|
|
14
16
|
const handlerStates = new Map();
|
|
15
17
|
function getState(id) {
|
|
@@ -31,50 +33,76 @@ function getHandlerStates() {
|
|
|
31
33
|
/**
|
|
32
34
|
* Execute all handlers for an event in parallel.
|
|
33
35
|
* Returns merged results array.
|
|
36
|
+
* Optionally accepts pre-fetched context for LLM prompt rendering.
|
|
34
37
|
*/
|
|
35
|
-
async function executeHandlers(_event, input, handlers) {
|
|
36
|
-
|
|
38
|
+
async function executeHandlers(_event, input, handlers, context) {
|
|
39
|
+
// Separate LLM handlers from script/inline, applying shared pre-checks
|
|
40
|
+
const llmHandlers = [];
|
|
41
|
+
const otherPromises = [];
|
|
42
|
+
const skippedResults = [];
|
|
43
|
+
for (const handler of handlers) {
|
|
37
44
|
// Skip disabled handlers (both manifest-disabled and auto-disabled)
|
|
38
45
|
if (handler.enabled === false) {
|
|
39
|
-
|
|
46
|
+
skippedResults.push({ id: handler.id, ok: true, output: undefined, duration_ms: 0 });
|
|
47
|
+
continue;
|
|
40
48
|
}
|
|
41
49
|
const state = getState(handler.id);
|
|
42
50
|
if (state.disabled) {
|
|
43
|
-
|
|
51
|
+
skippedResults.push({
|
|
44
52
|
id: handler.id,
|
|
45
53
|
ok: false,
|
|
46
54
|
error: `Auto-disabled after ${constants_js_1.MAX_CONSECUTIVE_FAILURES} consecutive failures`,
|
|
47
55
|
duration_ms: 0,
|
|
48
|
-
};
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
result = await executeScriptHandler(handler, input);
|
|
56
|
-
}
|
|
57
|
-
else if (handler.type === 'inline') {
|
|
58
|
-
result = await executeInlineHandler(handler, input);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
result = {
|
|
59
|
+
// Evaluate keyword filter before execution
|
|
60
|
+
if (handler.filter) {
|
|
61
|
+
const inputStr = JSON.stringify(input);
|
|
62
|
+
if (!(0, filter_js_1.evaluateFilter)(handler.filter, inputStr)) {
|
|
63
|
+
skippedResults.push({
|
|
62
64
|
id: handler.id,
|
|
63
|
-
ok:
|
|
64
|
-
|
|
65
|
+
ok: true,
|
|
66
|
+
output: undefined,
|
|
65
67
|
duration_ms: 0,
|
|
66
|
-
|
|
68
|
+
filtered: true,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
state.totalFires++;
|
|
74
|
+
if (handler.type === 'llm') {
|
|
75
|
+
llmHandlers.push(handler);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Execute script/inline handlers in parallel
|
|
79
|
+
otherPromises.push(executeOtherHandler(handler, input));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Execute script/inline handlers in parallel
|
|
83
|
+
const otherResults = otherPromises.length > 0
|
|
84
|
+
? await Promise.all(otherPromises)
|
|
85
|
+
: [];
|
|
86
|
+
// Execute LLM handlers with batching (graceful — never crashes)
|
|
87
|
+
let llmResults = [];
|
|
88
|
+
if (llmHandlers.length > 0) {
|
|
89
|
+
try {
|
|
90
|
+
llmResults = await (0, llm_js_1.executeLLMHandlersBatched)(llmHandlers, input, context ?? {});
|
|
91
|
+
}
|
|
69
92
|
catch (err) {
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
// Graceful degradation: if LLM execution entirely fails, return error results
|
|
94
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
llmResults = llmHandlers.map(h => ({
|
|
96
|
+
id: h.id,
|
|
72
97
|
ok: false,
|
|
73
|
-
error:
|
|
74
|
-
duration_ms:
|
|
75
|
-
};
|
|
98
|
+
error: `LLM execution failed: ${errorMsg}`,
|
|
99
|
+
duration_ms: 0,
|
|
100
|
+
}));
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
}
|
|
103
|
+
// Update failure tracking for all executed results
|
|
104
|
+
for (const result of [...otherResults, ...llmResults]) {
|
|
105
|
+
const state = getState(result.id);
|
|
78
106
|
if (result.ok) {
|
|
79
107
|
state.consecutiveFailures = 0;
|
|
80
108
|
}
|
|
@@ -85,19 +113,49 @@ async function executeHandlers(_event, input, handlers) {
|
|
|
85
113
|
state.disabled = true;
|
|
86
114
|
}
|
|
87
115
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
}
|
|
117
|
+
return [...skippedResults, ...otherResults, ...llmResults];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute a single script or inline handler with error handling.
|
|
121
|
+
*/
|
|
122
|
+
async function executeOtherHandler(handler, input) {
|
|
123
|
+
const start = performance.now();
|
|
124
|
+
try {
|
|
125
|
+
if (handler.type === 'script') {
|
|
126
|
+
return await executeScriptHandler(handler, input);
|
|
127
|
+
}
|
|
128
|
+
else if (handler.type === 'inline') {
|
|
129
|
+
return await executeInlineHandler(handler, input);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return {
|
|
133
|
+
id: handler.id,
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `Unknown handler type: ${handler.type}`,
|
|
136
|
+
duration_ms: 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
return {
|
|
142
|
+
id: handler.id,
|
|
143
|
+
ok: false,
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
duration_ms: performance.now() - start,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
91
148
|
}
|
|
92
149
|
/**
|
|
93
150
|
* Execute a script handler: spawn a child process, pipe input JSON to stdin,
|
|
94
151
|
* read stdout as JSON response.
|
|
95
152
|
*/
|
|
96
153
|
function executeScriptHandler(handler, input) {
|
|
97
|
-
const
|
|
154
|
+
const h = handler;
|
|
155
|
+
const timeout = h.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
|
|
98
156
|
return new Promise((resolve) => {
|
|
99
157
|
const start = performance.now();
|
|
100
|
-
const child = (0, child_process_1.spawn)('sh', ['-c',
|
|
158
|
+
const child = (0, child_process_1.spawn)('sh', ['-c', h.command], {
|
|
101
159
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
102
160
|
timeout,
|
|
103
161
|
});
|
|
@@ -155,17 +213,18 @@ function executeScriptHandler(handler, input) {
|
|
|
155
213
|
* Execute an inline handler: dynamically import a JS module and call its default export.
|
|
156
214
|
*/
|
|
157
215
|
async function executeInlineHandler(handler, input) {
|
|
158
|
-
const
|
|
216
|
+
const h = handler;
|
|
217
|
+
const timeout = h.timeout ?? constants_js_1.DEFAULT_HANDLER_TIMEOUT;
|
|
159
218
|
const start = performance.now();
|
|
160
219
|
try {
|
|
161
|
-
const modulePath = (0, path_1.resolve)(
|
|
220
|
+
const modulePath = (0, path_1.resolve)(h.module);
|
|
162
221
|
const moduleUrl = (0, url_1.pathToFileURL)(modulePath).href;
|
|
163
222
|
const mod = await import(moduleUrl);
|
|
164
223
|
if (typeof mod.default !== 'function') {
|
|
165
224
|
return {
|
|
166
|
-
id:
|
|
225
|
+
id: h.id,
|
|
167
226
|
ok: false,
|
|
168
|
-
error: `Module "${
|
|
227
|
+
error: `Module "${h.module}" does not export a default function`,
|
|
169
228
|
duration_ms: performance.now() - start,
|
|
170
229
|
};
|
|
171
230
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,8 @@ export { migrate, restore, getSettingsPath } from './migrate.js';
|
|
|
5
5
|
export type { MigratePathOptions } from './migrate.js';
|
|
6
6
|
export { runDoctor } from './doctor.js';
|
|
7
7
|
export { executeHandlers } from './handlers.js';
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
8
|
+
export { evaluateFilter } from './filter.js';
|
|
9
|
+
export { executeLLMHandler, executeLLMHandlersBatched, calculateCost, resetClient } from './llm.js';
|
|
10
|
+
export { prefetchContext, renderPromptTemplate } from './prefetch.js';
|
|
11
|
+
export { DEFAULT_PORT, CONFIG_DIR, MANIFEST_PATH, PID_FILE, METRICS_FILE, LOG_FILE, COSTS_FILE, DEFAULT_LLM_TIMEOUT, DEFAULT_LLM_MAX_TOKENS, LLM_PRICING } from './constants.js';
|
|
12
|
+
export type { HookEvent, HookInput, HandlerType, HandlerConfig, ScriptHandlerConfig, InlineHandlerConfig, LLMHandlerConfig, LLMModel, Manifest, HandlerResult, MetricEntry, HandlerState, DiagnosticResult, PrefetchKey, PrefetchContext, TokenUsage, CostEntry, } from './types.js';
|