@mauribadnights/clooks 0.3.2 → 0.4.1
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 +276 -201
- package/agents/clooks.md +146 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +43 -0
- package/dist/cli.js +126 -17
- package/dist/doctor.js +20 -0
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +105 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10 -2
- package/dist/manifest.js +33 -0
- package/dist/server.d.ts +21 -1
- package/dist/server.js +126 -8
- package/dist/service.d.ts +27 -0
- package/dist/service.js +242 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +730 -0
- package/dist/types.d.ts +11 -0
- package/docs/DASHBOARD-VISION.md +66 -0
- package/docs/DEFERRED-FIXES.md +35 -0
- package/docs/architecture.png +0 -0
- package/docs/generate-diagram.py +177 -0
- package/package.json +3 -1
package/agents/clooks.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clooks
|
|
3
|
+
description: Expert assistant for clooks — the persistent hook runtime for Claude Code
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the clooks expert agent. You have deep knowledge of the clooks architecture, configuration, and troubleshooting.
|
|
8
|
+
|
|
9
|
+
## What is clooks
|
|
10
|
+
|
|
11
|
+
clooks is a persistent HTTP daemon (localhost:7890) that handles Claude Code hooks. Instead of spawning a fresh process for every hook invocation, Claude Code POSTs to the daemon, which dispatches to handlers defined in ~/.clooks/manifest.yaml. This eliminates cold-start overhead (112x faster) and provides observability, filtering, LLM batching, and more.
|
|
12
|
+
|
|
13
|
+
## Your capabilities
|
|
14
|
+
|
|
15
|
+
You can read and modify the user's clooks configuration:
|
|
16
|
+
- **Manifest:** ~/.clooks/manifest.yaml — defines all handlers
|
|
17
|
+
- **Daemon log:** ~/.clooks/daemon.log — server output and errors
|
|
18
|
+
- **Metrics:** ~/.clooks/metrics.jsonl — execution metrics (fires, errors, latency)
|
|
19
|
+
- **Costs:** ~/.clooks/costs.jsonl — LLM token usage and costs
|
|
20
|
+
- **Plugins:** ~/.clooks/plugins/ — installed plugins with their own handlers
|
|
21
|
+
- **Settings:** ~/.claude/settings.json — Claude Code hook entries pointing to the daemon
|
|
22
|
+
|
|
23
|
+
You can run clooks CLI commands:
|
|
24
|
+
- `clooks doctor` — diagnose issues
|
|
25
|
+
- `clooks stats -t` — show metrics (text mode since you're an agent)
|
|
26
|
+
- `clooks costs` — show LLM cost breakdown
|
|
27
|
+
- `clooks status` — daemon status
|
|
28
|
+
- `clooks plugins` — list installed plugins
|
|
29
|
+
- `clooks service status` — system service status
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
### Handler types
|
|
34
|
+
- **script** — spawns `sh -c "command"`, pipes hook JSON to stdin, reads stdout (~5-35ms)
|
|
35
|
+
- **inline** — imports a JS module, calls default export in-process (~0ms after first load)
|
|
36
|
+
- **llm** — calls Anthropic Messages API with prompt template and $VARIABLE interpolation
|
|
37
|
+
|
|
38
|
+
### Handler fields
|
|
39
|
+
Every handler has:
|
|
40
|
+
- `id` (required) — unique identifier
|
|
41
|
+
- `type` (required) — script, inline, or llm
|
|
42
|
+
- `filter` — keyword filter: "word1|word2|!word3" (OR logic, ! negates, case-insensitive)
|
|
43
|
+
- `project` — glob against cwd: "*/Driffusion/*" (only fire in matching directories)
|
|
44
|
+
- `agent` — agent name match: "builder,coo" (only fire in matching agent sessions)
|
|
45
|
+
- `depends` — array of handler IDs this handler waits for
|
|
46
|
+
- `async` — boolean, fire-and-forget (doesn't block Claude's response)
|
|
47
|
+
- `sessionIsolation` — boolean, reset handler state on SessionStart
|
|
48
|
+
- `timeout` — milliseconds
|
|
49
|
+
- `enabled` — boolean
|
|
50
|
+
|
|
51
|
+
### LLM handler extra fields
|
|
52
|
+
- `model` — claude-haiku-4-5, claude-sonnet-4-6, or claude-opus-4-6
|
|
53
|
+
- `prompt` — template with $TRANSCRIPT, $GIT_STATUS, $GIT_DIFF, $ARGUMENTS, $TOOL_NAME, $PROMPT, $CWD
|
|
54
|
+
- `batchGroup` — handlers with same group + same session = one API call
|
|
55
|
+
- `maxTokens`, `temperature`
|
|
56
|
+
|
|
57
|
+
### Plugin system
|
|
58
|
+
Plugins ship a `clooks-plugin.yaml` with handlers. Install via `clooks add <path>`. Handler IDs are namespaced as `pluginName/handlerId`. Manage with `clooks plugins`, `clooks remove <name>`.
|
|
59
|
+
|
|
60
|
+
### Dependency resolution
|
|
61
|
+
Handlers with `depends: [other-handler-id]` execute in topological order (waves). Wave 0 = no deps, Wave 1 = depends on Wave 0, etc. Parallel within each wave.
|
|
62
|
+
|
|
63
|
+
### Short-circuit chains
|
|
64
|
+
PreToolUse deny → PostToolUse handlers auto-skipped for that tool call (30s TTL cache).
|
|
65
|
+
|
|
66
|
+
### Manifest format
|
|
67
|
+
```yaml
|
|
68
|
+
handlers:
|
|
69
|
+
PreToolUse:
|
|
70
|
+
- id: safety-guard
|
|
71
|
+
type: script
|
|
72
|
+
command: node ~/hooks/guard.js
|
|
73
|
+
filter: "Bash|!Read"
|
|
74
|
+
project: "*/my-project/*"
|
|
75
|
+
timeout: 3000
|
|
76
|
+
|
|
77
|
+
UserPromptSubmit:
|
|
78
|
+
- id: learning-detector
|
|
79
|
+
type: llm
|
|
80
|
+
model: claude-haiku-4-5
|
|
81
|
+
prompt: "Analyze for learning evidence: $PROMPT"
|
|
82
|
+
batchGroup: analysis
|
|
83
|
+
async: true
|
|
84
|
+
|
|
85
|
+
Stop:
|
|
86
|
+
- id: session-logger
|
|
87
|
+
type: inline
|
|
88
|
+
module: ~/.clooks/handlers/logger.js
|
|
89
|
+
|
|
90
|
+
prefetch:
|
|
91
|
+
- transcript
|
|
92
|
+
- git_status
|
|
93
|
+
|
|
94
|
+
settings:
|
|
95
|
+
port: 7890
|
|
96
|
+
logLevel: info
|
|
97
|
+
authToken: abc123
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Settings.json integration
|
|
101
|
+
Claude Code settings.json has HTTP hooks pointing to the daemon:
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"hooks": {
|
|
105
|
+
"PostToolUse": [{ "hooks": [{ "type": "http", "url": "http://localhost:7890/hooks/PostToolUse" }] }]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
SessionStart includes a command hook for `clooks ensure-running` that auto-starts the daemon.
|
|
110
|
+
`clooks sync` ensures settings.json has HTTP hooks for all events with handlers.
|
|
111
|
+
|
|
112
|
+
### System service
|
|
113
|
+
`clooks service install` creates a launchd plist (macOS), systemd unit (Linux), or scheduled task (Windows) that keeps the daemon alive across sleep/wake/crashes.
|
|
114
|
+
|
|
115
|
+
## How to help users
|
|
116
|
+
|
|
117
|
+
### Diagnosing issues
|
|
118
|
+
1. Run `clooks doctor` — check for errors/warnings
|
|
119
|
+
2. Read ~/.clooks/daemon.log — look for errors, auth failures, parse failures
|
|
120
|
+
3. Run `clooks stats -t` — check for high error rates or slow handlers
|
|
121
|
+
4. Check ~/.clooks/manifest.yaml — validate handler configs
|
|
122
|
+
5. Check ~/.claude/settings.json — verify HTTP hooks point to localhost:7890
|
|
123
|
+
|
|
124
|
+
### Common problems
|
|
125
|
+
- **ECONNREFUSED** — daemon not running. `clooks start` or `clooks service install`
|
|
126
|
+
- **HTTP 401** — auth token mismatch between manifest and settings.json. `clooks rotate-token`
|
|
127
|
+
- **HTTP 429** — rate limited from too many auth failures. Restart daemon: `clooks stop && clooks start`
|
|
128
|
+
- **Handler always filtered** — check filter/project/agent fields, run with `clooks stats -t` to see filtered count
|
|
129
|
+
- **Slow handler** — check avg ms in stats. Consider `async: true` if it doesn't need to inject context
|
|
130
|
+
- **Stale PID** — `rm ~/.clooks/daemon.pid && clooks start`
|
|
131
|
+
|
|
132
|
+
### Optimizing performance
|
|
133
|
+
- Convert Python script handlers to inline JS handlers (1000ms → <1ms)
|
|
134
|
+
- Mark non-blocking handlers as `async: true`
|
|
135
|
+
- Use `filter` to skip irrelevant invocations
|
|
136
|
+
- Use `project`/`agent` to scope handlers to relevant contexts
|
|
137
|
+
- Batch LLM handlers with `batchGroup`
|
|
138
|
+
- Use `prefetch` to avoid redundant file reads
|
|
139
|
+
|
|
140
|
+
### Writing new handlers
|
|
141
|
+
Help users write handlers for their specific needs. Always:
|
|
142
|
+
- Generate unique, descriptive IDs
|
|
143
|
+
- Set appropriate timeouts
|
|
144
|
+
- Add filters when the handler doesn't need every invocation
|
|
145
|
+
- Use async for analysis/logging that doesn't affect Claude's response
|
|
146
|
+
- Test with `echo '{"session_id":"test","cwd":"/tmp","hook_event_name":"PostToolUse"}' | node handler.js`
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install/update the clooks agent to ~/.claude/agents/clooks.md.
|
|
3
|
+
* Returns true if installed/updated, false if already up to date.
|
|
4
|
+
*/
|
|
5
|
+
export declare function installAgent(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Check if the clooks agent is installed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isAgentInstalled(): boolean;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// clooks agent — auto-install the clooks expert agent to ~/.claude/agents/
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.installAgent = installAgent;
|
|
5
|
+
exports.isAgentInstalled = isAgentInstalled;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const os_1 = require("os");
|
|
9
|
+
const AGENT_SOURCE = (0, path_1.join)(__dirname, '..', 'agents', 'clooks.md');
|
|
10
|
+
const AGENT_DEST_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'agents');
|
|
11
|
+
const AGENT_DEST = (0, path_1.join)(AGENT_DEST_DIR, 'clooks.md');
|
|
12
|
+
/**
|
|
13
|
+
* Install/update the clooks agent to ~/.claude/agents/clooks.md.
|
|
14
|
+
* Returns true if installed/updated, false if already up to date.
|
|
15
|
+
*/
|
|
16
|
+
function installAgent() {
|
|
17
|
+
// Read source agent from package
|
|
18
|
+
let source;
|
|
19
|
+
try {
|
|
20
|
+
source = (0, fs_1.readFileSync)(AGENT_SOURCE, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false; // Agent file not found in package
|
|
24
|
+
}
|
|
25
|
+
// Check if already installed and identical
|
|
26
|
+
if ((0, fs_1.existsSync)(AGENT_DEST)) {
|
|
27
|
+
const existing = (0, fs_1.readFileSync)(AGENT_DEST, 'utf-8');
|
|
28
|
+
if (existing === source)
|
|
29
|
+
return false; // Already up to date
|
|
30
|
+
}
|
|
31
|
+
// Install/update
|
|
32
|
+
if (!(0, fs_1.existsSync)(AGENT_DEST_DIR)) {
|
|
33
|
+
(0, fs_1.mkdirSync)(AGENT_DEST_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
(0, fs_1.writeFileSync)(AGENT_DEST, source, 'utf-8');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if the clooks agent is installed.
|
|
40
|
+
*/
|
|
41
|
+
function isAgentInstalled() {
|
|
42
|
+
return (0, fs_1.existsSync)(AGENT_DEST);
|
|
43
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -11,14 +11,17 @@ const doctor_js_1 = require("./doctor.js");
|
|
|
11
11
|
const auth_js_1 = require("./auth.js");
|
|
12
12
|
const plugin_js_1 = require("./plugin.js");
|
|
13
13
|
const sync_js_1 = require("./sync.js");
|
|
14
|
+
const service_js_1 = require("./service.js");
|
|
14
15
|
const constants_js_1 = require("./constants.js");
|
|
16
|
+
const tui_js_1 = require("./tui.js");
|
|
17
|
+
const agent_js_1 = require("./agent.js");
|
|
15
18
|
const fs_1 = require("fs");
|
|
16
19
|
const path_1 = require("path");
|
|
17
20
|
const program = new commander_1.Command();
|
|
18
21
|
program
|
|
19
22
|
.name('clooks')
|
|
20
23
|
.description('Persistent hook runtime for Claude Code')
|
|
21
|
-
.version('0.
|
|
24
|
+
.version('0.4.1');
|
|
22
25
|
// --- start ---
|
|
23
26
|
program
|
|
24
27
|
.command('start')
|
|
@@ -28,10 +31,18 @@ program
|
|
|
28
31
|
.action(async (opts) => {
|
|
29
32
|
const noWatch = opts.watch === false;
|
|
30
33
|
if (!opts.foreground) {
|
|
31
|
-
// Background mode: check if already running
|
|
34
|
+
// Background mode: check if already running and healthy
|
|
32
35
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
const healthy = await (0, server_js_1.isDaemonHealthy)();
|
|
37
|
+
if (healthy) {
|
|
38
|
+
console.log('Daemon is already running.');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
// PID alive but daemon unhealthy — stale process after sleep/lid-close
|
|
42
|
+
const stalePid = (0, server_js_1.cleanupStaleDaemon)();
|
|
43
|
+
if (stalePid) {
|
|
44
|
+
console.log(`Cleaned up stale daemon (pid ${stalePid}), starting fresh`);
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
// Ensure config dir exists
|
|
37
48
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
@@ -94,6 +105,8 @@ program
|
|
|
94
105
|
}
|
|
95
106
|
const pid = (0, fs_1.existsSync)(constants_js_1.PID_FILE) ? (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim() : '?';
|
|
96
107
|
// Try to hit health endpoint
|
|
108
|
+
// Service status
|
|
109
|
+
const serviceStatus = (0, service_js_1.getServiceStatus)();
|
|
97
110
|
try {
|
|
98
111
|
const { get } = await import('http');
|
|
99
112
|
const data = await new Promise((resolve, reject) => {
|
|
@@ -113,9 +126,11 @@ program
|
|
|
113
126
|
console.log(`Uptime: ${formatUptime(health.uptime)}`);
|
|
114
127
|
console.log(`Handlers loaded: ${health.handlers_loaded}`);
|
|
115
128
|
console.log(`Plugins: ${pluginCount}`);
|
|
129
|
+
console.log(`Service: ${serviceStatus}`);
|
|
116
130
|
}
|
|
117
131
|
catch {
|
|
118
132
|
console.log(`Status: running (pid ${pid})`);
|
|
133
|
+
console.log(`Service: ${serviceStatus}`);
|
|
119
134
|
console.log(`Note: Could not reach health endpoint on port ${constants_js_1.DEFAULT_PORT}`);
|
|
120
135
|
}
|
|
121
136
|
});
|
|
@@ -123,17 +138,25 @@ program
|
|
|
123
138
|
program
|
|
124
139
|
.command('stats')
|
|
125
140
|
.description('Show hook execution metrics')
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Append cost summary if LLM data exists
|
|
133
|
-
const costStats = metrics.getCostStats();
|
|
134
|
-
if (costStats.totalCost > 0) {
|
|
141
|
+
.option('-t, --text', 'Plain text output (default when piped)')
|
|
142
|
+
.action((opts) => {
|
|
143
|
+
if (opts.text || !process.stdout.isTTY) {
|
|
144
|
+
// Text output (forced via flag or non-TTY/piped stdout)
|
|
145
|
+
const metrics = new metrics_js_1.MetricsCollector();
|
|
146
|
+
console.log(metrics.formatStatsTable());
|
|
135
147
|
console.log('');
|
|
136
|
-
console.log(
|
|
148
|
+
console.log('Per Handler:');
|
|
149
|
+
console.log(metrics.formatHandlerStatsTable());
|
|
150
|
+
// Append cost summary if LLM data exists
|
|
151
|
+
const costStats = metrics.getCostStats();
|
|
152
|
+
if (costStats.totalCost > 0) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(metrics.formatCostTable());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Interactive TUI (default for terminals)
|
|
159
|
+
(0, tui_js_1.launchDashboard)();
|
|
137
160
|
}
|
|
138
161
|
});
|
|
139
162
|
// --- costs ---
|
|
@@ -157,6 +180,20 @@ program
|
|
|
157
180
|
console.log(` Handlers created: ${result.handlersCreated}`);
|
|
158
181
|
console.log(` Backup: ~/.clooks/settings.backup.json`);
|
|
159
182
|
console.log('\nRun "clooks start" to start the daemon.');
|
|
183
|
+
// Auto-install clooks agent
|
|
184
|
+
if ((0, agent_js_1.installAgent)()) {
|
|
185
|
+
console.log('Agent updated: claude --agent clooks');
|
|
186
|
+
}
|
|
187
|
+
// Auto-install system service
|
|
188
|
+
if (!(0, service_js_1.isServiceInstalled)()) {
|
|
189
|
+
try {
|
|
190
|
+
(0, service_js_1.installService)();
|
|
191
|
+
console.log('System service installed (auto-start on login, auto-restart on crash).');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
console.log('Note: Could not install system service. Run "clooks service install" manually.');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
160
197
|
}
|
|
161
198
|
catch (err) {
|
|
162
199
|
console.error('Migration failed:', err instanceof Error ? err.message : err);
|
|
@@ -216,9 +253,25 @@ program
|
|
|
216
253
|
.description('Start daemon if not already running (used by SessionStart hook)')
|
|
217
254
|
.action(async () => {
|
|
218
255
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
256
|
+
const healthy = await (0, server_js_1.isDaemonHealthy)();
|
|
257
|
+
if (healthy) {
|
|
258
|
+
// Already running and healthy — sync settings silently and exit fast
|
|
259
|
+
(0, sync_js_1.syncSettings)();
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
// PID alive but daemon unhealthy — stale process after sleep/lid-close
|
|
263
|
+
const stalePid = (0, server_js_1.cleanupStaleDaemon)();
|
|
264
|
+
if (stalePid) {
|
|
265
|
+
// Log to daemon.log for visibility
|
|
266
|
+
const { appendFileSync } = await import('fs');
|
|
267
|
+
const { LOG_FILE } = await import('./constants.js');
|
|
268
|
+
try {
|
|
269
|
+
appendFileSync(LOG_FILE, `[${new Date().toISOString()}] Cleaned up stale daemon (pid ${stalePid}), starting fresh\n`, 'utf-8');
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// ignore
|
|
273
|
+
}
|
|
274
|
+
}
|
|
222
275
|
}
|
|
223
276
|
// Ensure config dir exists
|
|
224
277
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
@@ -247,6 +300,20 @@ program
|
|
|
247
300
|
console.log(`Created: ${path}`);
|
|
248
301
|
console.log(`Auth token: ${token}`);
|
|
249
302
|
console.log('Edit this file to configure your hook handlers.');
|
|
303
|
+
// Auto-install clooks agent
|
|
304
|
+
if ((0, agent_js_1.installAgent)()) {
|
|
305
|
+
console.log('Agent updated: claude --agent clooks');
|
|
306
|
+
}
|
|
307
|
+
// Auto-install system service
|
|
308
|
+
if (!(0, service_js_1.isServiceInstalled)()) {
|
|
309
|
+
try {
|
|
310
|
+
(0, service_js_1.installService)();
|
|
311
|
+
console.log('System service installed (auto-start on login, auto-restart on crash).');
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
console.log('Note: Could not install system service. Run "clooks service install" manually.');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
250
317
|
});
|
|
251
318
|
// --- rotate-token ---
|
|
252
319
|
program
|
|
@@ -281,6 +348,10 @@ program
|
|
|
281
348
|
console.log(`Updating: ${currentVersion} \u2192 ${latest}`);
|
|
282
349
|
execSync('npm install -g @mauribadnights/clooks@latest', { stdio: 'inherit' });
|
|
283
350
|
console.log(`Updated to ${latest}.`);
|
|
351
|
+
// Auto-install/update clooks agent
|
|
352
|
+
if ((0, agent_js_1.installAgent)()) {
|
|
353
|
+
console.log('Agent updated: claude --agent clooks');
|
|
354
|
+
}
|
|
284
355
|
// Restart daemon if running
|
|
285
356
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
286
357
|
console.log('Restarting daemon...');
|
|
@@ -375,6 +446,44 @@ program
|
|
|
375
446
|
}
|
|
376
447
|
}
|
|
377
448
|
});
|
|
449
|
+
// --- service ---
|
|
450
|
+
const service = program.command('service').description('Manage clooks system service');
|
|
451
|
+
service.command('install')
|
|
452
|
+
.description('Install as system service (auto-start, auto-restart)')
|
|
453
|
+
.action(() => {
|
|
454
|
+
try {
|
|
455
|
+
(0, service_js_1.installService)();
|
|
456
|
+
console.log('Service installed. clooks will now:');
|
|
457
|
+
console.log(' - Start automatically on login');
|
|
458
|
+
console.log(' - Restart automatically if it crashes');
|
|
459
|
+
console.log(' - Survive sleep/wake cycles');
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
console.error('Failed to install service:', err instanceof Error ? err.message : err);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
service.command('uninstall')
|
|
467
|
+
.description('Remove system service')
|
|
468
|
+
.action(() => {
|
|
469
|
+
try {
|
|
470
|
+
(0, service_js_1.uninstallService)();
|
|
471
|
+
console.log('Service removed.');
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
console.error('Failed to uninstall service:', err instanceof Error ? err.message : err);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
service.command('status')
|
|
479
|
+
.description('Show service status')
|
|
480
|
+
.action(() => {
|
|
481
|
+
const status = (0, service_js_1.getServiceStatus)();
|
|
482
|
+
console.log(`Service: ${status}`);
|
|
483
|
+
if (status === 'not-installed') {
|
|
484
|
+
console.log('Run "clooks service install" to install.');
|
|
485
|
+
}
|
|
486
|
+
});
|
|
378
487
|
program.parse();
|
|
379
488
|
function formatUptime(seconds) {
|
|
380
489
|
if (seconds < 60)
|
package/dist/doctor.js
CHANGED
|
@@ -10,7 +10,9 @@ const os_1 = require("os");
|
|
|
10
10
|
const constants_js_1 = require("./constants.js");
|
|
11
11
|
const manifest_js_1 = require("./manifest.js");
|
|
12
12
|
const server_js_1 = require("./server.js");
|
|
13
|
+
const service_js_1 = require("./service.js");
|
|
13
14
|
const plugin_js_1 = require("./plugin.js");
|
|
15
|
+
const agent_js_1 = require("./agent.js");
|
|
14
16
|
const yaml_1 = require("yaml");
|
|
15
17
|
/**
|
|
16
18
|
* Run all diagnostic checks and return results.
|
|
@@ -35,6 +37,10 @@ async function runDoctor() {
|
|
|
35
37
|
results.push(checkAuthToken());
|
|
36
38
|
// 9. Plugin health checks
|
|
37
39
|
results.push(...checkPluginHealth());
|
|
40
|
+
// 10. System service
|
|
41
|
+
results.push(checkService());
|
|
42
|
+
// 11. clooks agent
|
|
43
|
+
results.push(checkAgent());
|
|
38
44
|
return results;
|
|
39
45
|
}
|
|
40
46
|
function checkConfigDir() {
|
|
@@ -276,3 +282,17 @@ function checkPluginHealth() {
|
|
|
276
282
|
}
|
|
277
283
|
return results;
|
|
278
284
|
}
|
|
285
|
+
function checkService() {
|
|
286
|
+
const status = (0, service_js_1.getServiceStatus)();
|
|
287
|
+
if (status === 'running')
|
|
288
|
+
return { check: 'System service', status: 'ok', message: 'Installed and running' };
|
|
289
|
+
if (status === 'stopped')
|
|
290
|
+
return { check: 'System service', status: 'warn', message: 'Installed but not running' };
|
|
291
|
+
return { check: 'System service', status: 'warn', message: 'Not installed. Run "clooks service install" for auto-restart.' };
|
|
292
|
+
}
|
|
293
|
+
function checkAgent() {
|
|
294
|
+
if ((0, agent_js_1.isAgentInstalled)()) {
|
|
295
|
+
return { check: 'clooks agent', status: 'ok', message: 'Installed at ~/.claude/agents/clooks.md' };
|
|
296
|
+
}
|
|
297
|
+
return { check: 'clooks agent', status: 'warn', message: 'Not installed. Run "clooks init" to install.' };
|
|
298
|
+
}
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput, PrefetchContext } from './types.js';
|
|
2
|
+
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
3
|
+
declare function matchAgent(pattern: string, currentAgent: string): boolean;
|
|
4
|
+
/** Match handler project field against cwd path */
|
|
5
|
+
declare function matchProject(pattern: string, cwd: string): boolean;
|
|
6
|
+
/** Exported for testing */
|
|
7
|
+
export { matchAgent, matchProject };
|
|
2
8
|
/** Reset all handler states (useful for testing) */
|
|
3
9
|
export declare function resetHandlerStates(): void;
|
|
4
10
|
/** Get a copy of the handler states map */
|
|
@@ -16,7 +22,7 @@ export declare function resetSessionIsolatedHandlers(handlers: HandlerConfig[]):
|
|
|
16
22
|
* Within each wave, handlers run in parallel.
|
|
17
23
|
* Outputs from previous waves are available to dependent handlers via _handlerOutputs.
|
|
18
24
|
*/
|
|
19
|
-
export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[], context?: PrefetchContext): Promise<HandlerResult[]>;
|
|
25
|
+
export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[], context?: PrefetchContext, onAsyncResult?: (result: HandlerResult) => void, currentAgent?: string): Promise<HandlerResult[]>;
|
|
20
26
|
/**
|
|
21
27
|
* Execute a script handler: spawn a child process, pipe input JSON to stdin,
|
|
22
28
|
* read stdout as JSON response.
|
package/dist/handlers.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// clooks hook handlers — execution engine
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.matchAgent = matchAgent;
|
|
5
|
+
exports.matchProject = matchProject;
|
|
4
6
|
exports.resetHandlerStates = resetHandlerStates;
|
|
5
7
|
exports.getHandlerStates = getHandlerStates;
|
|
6
8
|
exports.cleanupHandlerState = cleanupHandlerState;
|
|
@@ -15,6 +17,23 @@ const constants_js_1 = require("./constants.js");
|
|
|
15
17
|
const filter_js_1 = require("./filter.js");
|
|
16
18
|
const llm_js_1 = require("./llm.js");
|
|
17
19
|
const deps_js_1 = require("./deps.js");
|
|
20
|
+
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
21
|
+
function matchAgent(pattern, currentAgent) {
|
|
22
|
+
const agents = pattern.split(',').map(a => a.trim().toLowerCase());
|
|
23
|
+
return agents.includes(currentAgent.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
/** Match handler project field against cwd path */
|
|
26
|
+
function matchProject(pattern, cwd) {
|
|
27
|
+
if (!cwd)
|
|
28
|
+
return false;
|
|
29
|
+
// If pattern has wildcards, extract the literal parts and check includes
|
|
30
|
+
if (pattern.includes('*')) {
|
|
31
|
+
const parts = pattern.split('*').filter(Boolean);
|
|
32
|
+
return parts.every(part => cwd.includes(part));
|
|
33
|
+
}
|
|
34
|
+
// Exact match or prefix match
|
|
35
|
+
return cwd.startsWith(pattern) || cwd === pattern;
|
|
36
|
+
}
|
|
18
37
|
/** Runtime state per handler ID */
|
|
19
38
|
const handlerStates = new Map();
|
|
20
39
|
function getState(id) {
|
|
@@ -60,7 +79,7 @@ function resetSessionIsolatedHandlers(handlers) {
|
|
|
60
79
|
* Within each wave, handlers run in parallel.
|
|
61
80
|
* Outputs from previous waves are available to dependent handlers via _handlerOutputs.
|
|
62
81
|
*/
|
|
63
|
-
async function executeHandlers(_event, input, handlers, context) {
|
|
82
|
+
async function executeHandlers(_event, input, handlers, context, onAsyncResult, currentAgent) {
|
|
64
83
|
// Pre-check: filter out disabled/auto-disabled/filtered handlers before dep resolution
|
|
65
84
|
const eligible = [];
|
|
66
85
|
const skippedResults = [];
|
|
@@ -79,6 +98,20 @@ async function executeHandlers(_event, input, handlers, context) {
|
|
|
79
98
|
});
|
|
80
99
|
continue;
|
|
81
100
|
}
|
|
101
|
+
// Agent matching
|
|
102
|
+
if (handler.agent) {
|
|
103
|
+
if (!currentAgent || !matchAgent(handler.agent, currentAgent)) {
|
|
104
|
+
skippedResults.push({ id: handler.id, ok: true, duration_ms: 0, filtered: true });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Project matching (glob against cwd)
|
|
109
|
+
if (handler.project) {
|
|
110
|
+
if (!matchProject(handler.project, input.cwd)) {
|
|
111
|
+
skippedResults.push({ id: handler.id, ok: true, duration_ms: 0, filtered: true });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
82
115
|
if (handler.filter) {
|
|
83
116
|
const inputStr = JSON.stringify(input);
|
|
84
117
|
if (!(0, filter_js_1.evaluateFilter)(handler.filter, inputStr)) {
|
|
@@ -97,14 +130,83 @@ async function executeHandlers(_event, input, handlers, context) {
|
|
|
97
130
|
if (eligible.length === 0) {
|
|
98
131
|
return skippedResults;
|
|
99
132
|
}
|
|
133
|
+
// Separate async handlers from sync handlers
|
|
134
|
+
// Async handlers with dependents (or depended upon) are forced synchronous
|
|
135
|
+
const eligibleIds = new Set(eligible.map(h => h.id));
|
|
136
|
+
const dependedUpon = new Set();
|
|
137
|
+
for (const h of eligible) {
|
|
138
|
+
if (h.depends) {
|
|
139
|
+
for (const dep of h.depends) {
|
|
140
|
+
if (eligibleIds.has(dep))
|
|
141
|
+
dependedUpon.add(dep);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const syncHandlers = [];
|
|
146
|
+
const asyncHandlers = [];
|
|
147
|
+
for (const handler of eligible) {
|
|
148
|
+
if (handler.async) {
|
|
149
|
+
const hasDependents = dependedUpon.has(handler.id);
|
|
150
|
+
const hasDeps = handler.depends?.some(d => eligibleIds.has(d)) ?? false;
|
|
151
|
+
if (hasDependents || hasDeps) {
|
|
152
|
+
// Async handler has dependency relationships — force synchronous
|
|
153
|
+
console.warn(`[clooks] Warning: async handler "${handler.id}" has dependency relationships, running synchronously`);
|
|
154
|
+
syncHandlers.push(handler);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
asyncHandlers.push(handler);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
syncHandlers.push(handler);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Fire async handlers without awaiting
|
|
165
|
+
for (const handler of asyncHandlers) {
|
|
166
|
+
getState(handler.id).totalFires++;
|
|
167
|
+
if (handler.type === 'llm') {
|
|
168
|
+
(0, llm_js_1.executeLLMHandlersBatched)([handler], input, context ?? {}, input.session_id).then(results => {
|
|
169
|
+
for (const result of results) {
|
|
170
|
+
const state = getState(result.id);
|
|
171
|
+
if (result.ok)
|
|
172
|
+
state.consecutiveFailures = 0;
|
|
173
|
+
else {
|
|
174
|
+
state.consecutiveFailures++;
|
|
175
|
+
state.totalErrors++;
|
|
176
|
+
if (state.consecutiveFailures >= constants_js_1.MAX_CONSECUTIVE_FAILURES)
|
|
177
|
+
state.disabled = true;
|
|
178
|
+
}
|
|
179
|
+
onAsyncResult?.(result);
|
|
180
|
+
}
|
|
181
|
+
}).catch(() => { }); // never crash
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
executeOtherHandler(handler, input).then(result => {
|
|
185
|
+
const state = getState(result.id);
|
|
186
|
+
if (result.ok)
|
|
187
|
+
state.consecutiveFailures = 0;
|
|
188
|
+
else {
|
|
189
|
+
state.consecutiveFailures++;
|
|
190
|
+
state.totalErrors++;
|
|
191
|
+
if (state.consecutiveFailures >= constants_js_1.MAX_CONSECUTIVE_FAILURES)
|
|
192
|
+
state.disabled = true;
|
|
193
|
+
}
|
|
194
|
+
onAsyncResult?.(result);
|
|
195
|
+
}).catch(() => { }); // never crash
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Execute sync handlers with dependency resolution
|
|
199
|
+
if (syncHandlers.length === 0) {
|
|
200
|
+
return skippedResults;
|
|
201
|
+
}
|
|
100
202
|
// Resolve execution order into waves
|
|
101
203
|
let waves;
|
|
102
204
|
try {
|
|
103
|
-
waves = (0, deps_js_1.resolveExecutionOrder)(
|
|
205
|
+
waves = (0, deps_js_1.resolveExecutionOrder)(syncHandlers);
|
|
104
206
|
}
|
|
105
207
|
catch {
|
|
106
208
|
// If dep resolution fails, fall back to flat parallel execution
|
|
107
|
-
waves = [
|
|
209
|
+
waves = [syncHandlers];
|
|
108
210
|
}
|
|
109
211
|
const allResults = [...skippedResults];
|
|
110
212
|
const handlerOutputs = {};
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export { RateLimiter } from './ratelimit.js';
|
|
|
12
12
|
export { startWatcher, stopWatcher } from './watcher.js';
|
|
13
13
|
export { generateAuthToken, validateAuth, rotateToken } from './auth.js';
|
|
14
14
|
export { syncSettings } from './sync.js';
|
|
15
|
+
export { installService, uninstallService, isServiceInstalled, getServiceStatus } from './service.js';
|
|
16
|
+
export type { ServiceStatus } from './service.js';
|
|
15
17
|
export type { SyncOptions } from './sync.js';
|
|
16
18
|
export type { RotateTokenOptions } from './auth.js';
|
|
19
|
+
export { installAgent, isAgentInstalled } from './agent.js';
|
|
17
20
|
export { evaluateFilter } from './filter.js';
|
|
18
21
|
export { executeLLMHandler, executeLLMHandlersBatched, calculateCost, resetClient } from './llm.js';
|
|
19
22
|
export { prefetchContext, renderPromptTemplate } from './prefetch.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// clooks — public API exports
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
5
|
-
exports.PLUGIN_MANIFEST_NAME = exports.PLUGIN_REGISTRY = exports.PLUGINS_DIR = void 0;
|
|
4
|
+
exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = exports.renderPromptTemplate = exports.prefetchContext = exports.resetClient = exports.calculateCost = exports.executeLLMHandlersBatched = exports.executeLLMHandler = exports.evaluateFilter = exports.isAgentInstalled = exports.installAgent = exports.getServiceStatus = exports.isServiceInstalled = exports.uninstallService = exports.installService = exports.syncSettings = exports.rotateToken = exports.validateAuth = exports.generateAuthToken = exports.stopWatcher = exports.startWatcher = exports.RateLimiter = exports.DenyCache = exports.resolveExecutionOrder = exports.cleanupHandlerState = exports.resetSessionIsolatedHandlers = exports.executeHandlers = exports.runDoctor = exports.getSettingsPath = exports.restore = exports.migrate = exports.MetricsCollector = exports.listPlugins = exports.uninstallPlugin = exports.installPlugin = exports.saveRegistry = exports.loadRegistry = exports.validatePluginManifest = exports.mergeManifests = exports.loadPlugins = exports.createDefaultManifest = exports.validateManifest = exports.loadCompositeManifest = exports.loadManifest = exports.isDaemonRunning = exports.stopDaemon = exports.startDaemon = exports.createServer = void 0;
|
|
5
|
+
exports.PLUGIN_MANIFEST_NAME = exports.PLUGIN_REGISTRY = exports.PLUGINS_DIR = exports.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = exports.LOG_FILE = exports.METRICS_FILE = void 0;
|
|
6
6
|
var server_js_1 = require("./server.js");
|
|
7
7
|
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
|
|
8
8
|
Object.defineProperty(exports, "startDaemon", { enumerable: true, get: function () { return server_js_1.startDaemon; } });
|
|
@@ -49,6 +49,14 @@ Object.defineProperty(exports, "validateAuth", { enumerable: true, get: function
|
|
|
49
49
|
Object.defineProperty(exports, "rotateToken", { enumerable: true, get: function () { return auth_js_1.rotateToken; } });
|
|
50
50
|
var sync_js_1 = require("./sync.js");
|
|
51
51
|
Object.defineProperty(exports, "syncSettings", { enumerable: true, get: function () { return sync_js_1.syncSettings; } });
|
|
52
|
+
var service_js_1 = require("./service.js");
|
|
53
|
+
Object.defineProperty(exports, "installService", { enumerable: true, get: function () { return service_js_1.installService; } });
|
|
54
|
+
Object.defineProperty(exports, "uninstallService", { enumerable: true, get: function () { return service_js_1.uninstallService; } });
|
|
55
|
+
Object.defineProperty(exports, "isServiceInstalled", { enumerable: true, get: function () { return service_js_1.isServiceInstalled; } });
|
|
56
|
+
Object.defineProperty(exports, "getServiceStatus", { enumerable: true, get: function () { return service_js_1.getServiceStatus; } });
|
|
57
|
+
var agent_js_1 = require("./agent.js");
|
|
58
|
+
Object.defineProperty(exports, "installAgent", { enumerable: true, get: function () { return agent_js_1.installAgent; } });
|
|
59
|
+
Object.defineProperty(exports, "isAgentInstalled", { enumerable: true, get: function () { return agent_js_1.isAgentInstalled; } });
|
|
52
60
|
var filter_js_1 = require("./filter.js");
|
|
53
61
|
Object.defineProperty(exports, "evaluateFilter", { enumerable: true, get: function () { return filter_js_1.evaluateFilter; } });
|
|
54
62
|
var llm_js_1 = require("./llm.js");
|