@mauribadnights/clooks 0.3.1 → 0.4.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/agents/clooks.md +146 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +43 -0
- package/dist/cli.js +156 -16
- package/dist/doctor.js +20 -0
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +105 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -2
- package/dist/manifest.js +33 -0
- package/dist/ratelimit.d.ts +7 -2
- package/dist/ratelimit.js +25 -4
- package/dist/server.d.ts +21 -1
- package/dist/server.js +136 -12
- package/dist/service.d.ts +27 -0
- package/dist/service.js +242 -0
- package/dist/sync.d.ts +13 -0
- package/dist/sync.js +153 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +560 -0
- package/dist/types.d.ts +10 -0
- package/package.json +4 -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
|
@@ -10,14 +10,18 @@ const migrate_js_1 = require("./migrate.js");
|
|
|
10
10
|
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
|
+
const sync_js_1 = require("./sync.js");
|
|
14
|
+
const service_js_1 = require("./service.js");
|
|
13
15
|
const constants_js_1 = require("./constants.js");
|
|
16
|
+
const tui_js_1 = require("./tui.js");
|
|
17
|
+
const agent_js_1 = require("./agent.js");
|
|
14
18
|
const fs_1 = require("fs");
|
|
15
19
|
const path_1 = require("path");
|
|
16
20
|
const program = new commander_1.Command();
|
|
17
21
|
program
|
|
18
22
|
.name('clooks')
|
|
19
23
|
.description('Persistent hook runtime for Claude Code')
|
|
20
|
-
.version('0.
|
|
24
|
+
.version('0.4.0');
|
|
21
25
|
// --- start ---
|
|
22
26
|
program
|
|
23
27
|
.command('start')
|
|
@@ -27,10 +31,18 @@ program
|
|
|
27
31
|
.action(async (opts) => {
|
|
28
32
|
const noWatch = opts.watch === false;
|
|
29
33
|
if (!opts.foreground) {
|
|
30
|
-
// Background mode: check if already running
|
|
34
|
+
// Background mode: check if already running and healthy
|
|
31
35
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
|
34
46
|
}
|
|
35
47
|
// Ensure config dir exists
|
|
36
48
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
@@ -40,6 +52,11 @@ program
|
|
|
40
52
|
(0, server_js_1.startDaemonBackground)({ noWatch });
|
|
41
53
|
// Give it a moment to start
|
|
42
54
|
await new Promise((r) => setTimeout(r, 500));
|
|
55
|
+
// Sync settings.json with manifest
|
|
56
|
+
const syncAdded = (0, sync_js_1.syncSettings)();
|
|
57
|
+
if (syncAdded.length > 0) {
|
|
58
|
+
console.log(`Synced HTTP hooks for: ${syncAdded.join(', ')}`);
|
|
59
|
+
}
|
|
43
60
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
44
61
|
const pid = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
|
|
45
62
|
console.log(`Daemon started (pid ${pid}), listening on 127.0.0.1:${constants_js_1.DEFAULT_PORT}`);
|
|
@@ -88,6 +105,8 @@ program
|
|
|
88
105
|
}
|
|
89
106
|
const pid = (0, fs_1.existsSync)(constants_js_1.PID_FILE) ? (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim() : '?';
|
|
90
107
|
// Try to hit health endpoint
|
|
108
|
+
// Service status
|
|
109
|
+
const serviceStatus = (0, service_js_1.getServiceStatus)();
|
|
91
110
|
try {
|
|
92
111
|
const { get } = await import('http');
|
|
93
112
|
const data = await new Promise((resolve, reject) => {
|
|
@@ -107,9 +126,11 @@ program
|
|
|
107
126
|
console.log(`Uptime: ${formatUptime(health.uptime)}`);
|
|
108
127
|
console.log(`Handlers loaded: ${health.handlers_loaded}`);
|
|
109
128
|
console.log(`Plugins: ${pluginCount}`);
|
|
129
|
+
console.log(`Service: ${serviceStatus}`);
|
|
110
130
|
}
|
|
111
131
|
catch {
|
|
112
132
|
console.log(`Status: running (pid ${pid})`);
|
|
133
|
+
console.log(`Service: ${serviceStatus}`);
|
|
113
134
|
console.log(`Note: Could not reach health endpoint on port ${constants_js_1.DEFAULT_PORT}`);
|
|
114
135
|
}
|
|
115
136
|
});
|
|
@@ -117,17 +138,25 @@ program
|
|
|
117
138
|
program
|
|
118
139
|
.command('stats')
|
|
119
140
|
.description('Show hook execution metrics')
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Append cost summary if LLM data exists
|
|
127
|
-
const costStats = metrics.getCostStats();
|
|
128
|
-
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());
|
|
129
147
|
console.log('');
|
|
130
|
-
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)();
|
|
131
160
|
}
|
|
132
161
|
});
|
|
133
162
|
// --- costs ---
|
|
@@ -151,6 +180,20 @@ program
|
|
|
151
180
|
console.log(` Handlers created: ${result.handlersCreated}`);
|
|
152
181
|
console.log(` Backup: ~/.clooks/settings.backup.json`);
|
|
153
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
|
+
}
|
|
154
197
|
}
|
|
155
198
|
catch (err) {
|
|
156
199
|
console.error('Migration failed:', err instanceof Error ? err.message : err);
|
|
@@ -187,14 +230,48 @@ program
|
|
|
187
230
|
if (errors > 0)
|
|
188
231
|
process.exit(1);
|
|
189
232
|
});
|
|
233
|
+
// --- sync ---
|
|
234
|
+
program
|
|
235
|
+
.command('sync')
|
|
236
|
+
.description('Sync settings.json with manifest (add missing HTTP hook entries)')
|
|
237
|
+
.action(() => {
|
|
238
|
+
const added = (0, sync_js_1.syncSettings)();
|
|
239
|
+
if (added.length === 0) {
|
|
240
|
+
console.log('Settings already in sync.');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log(`Added HTTP hooks for: ${added.join(', ')}`);
|
|
244
|
+
const settingsPath = (0, migrate_js_1.getSettingsPath)();
|
|
245
|
+
if (settingsPath) {
|
|
246
|
+
console.log(`Settings updated: ${settingsPath}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
190
250
|
// --- ensure-running ---
|
|
191
251
|
program
|
|
192
252
|
.command('ensure-running')
|
|
193
253
|
.description('Start daemon if not already running (used by SessionStart hook)')
|
|
194
254
|
.action(async () => {
|
|
195
255
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
275
|
}
|
|
199
276
|
// Ensure config dir exists
|
|
200
277
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
@@ -206,6 +283,8 @@ program
|
|
|
206
283
|
(0, manifest_js_1.createDefaultManifest)();
|
|
207
284
|
}
|
|
208
285
|
(0, server_js_1.startDaemonBackground)();
|
|
286
|
+
// Sync settings silently after starting
|
|
287
|
+
(0, sync_js_1.syncSettings)();
|
|
209
288
|
process.exit(0);
|
|
210
289
|
});
|
|
211
290
|
// --- init ---
|
|
@@ -221,6 +300,20 @@ program
|
|
|
221
300
|
console.log(`Created: ${path}`);
|
|
222
301
|
console.log(`Auth token: ${token}`);
|
|
223
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
|
+
}
|
|
224
317
|
});
|
|
225
318
|
// --- rotate-token ---
|
|
226
319
|
program
|
|
@@ -255,6 +348,10 @@ program
|
|
|
255
348
|
console.log(`Updating: ${currentVersion} \u2192 ${latest}`);
|
|
256
349
|
execSync('npm install -g @mauribadnights/clooks@latest', { stdio: 'inherit' });
|
|
257
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
|
+
}
|
|
258
355
|
// Restart daemon if running
|
|
259
356
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
260
357
|
console.log('Restarting daemon...');
|
|
@@ -294,6 +391,11 @@ program
|
|
|
294
391
|
? Object.values(installed.manifest.handlers).reduce((sum, arr) => sum + (arr?.length ?? 0), 0)
|
|
295
392
|
: 0;
|
|
296
393
|
console.log(`Installed plugin ${plugin.name} v${plugin.version} (${handlerCount} handlers)`);
|
|
394
|
+
// Sync settings.json to add HTTP hooks for any new events
|
|
395
|
+
const syncAdded = (0, sync_js_1.syncSettings)();
|
|
396
|
+
if (syncAdded.length > 0) {
|
|
397
|
+
console.log(`Synced HTTP hooks for: ${syncAdded.join(', ')}`);
|
|
398
|
+
}
|
|
297
399
|
}
|
|
298
400
|
catch (err) {
|
|
299
401
|
console.error('Plugin install failed:', err instanceof Error ? err.message : err);
|
|
@@ -344,6 +446,44 @@ program
|
|
|
344
446
|
}
|
|
345
447
|
}
|
|
346
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
|
+
});
|
|
347
487
|
program.parse();
|
|
348
488
|
function formatUptime(seconds) {
|
|
349
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 = {};
|