@mauribadnights/clooks 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +61 -10
- package/dist/handlers.d.ts +2 -0
- package/dist/handlers.js +43 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +120 -11
- package/docs/.nojekyll +0 -0
- package/docs/_sidebar.md +32 -0
- package/docs/getting-started/installation.md +52 -0
- package/docs/getting-started/migration.md +68 -0
- package/docs/getting-started/quickstart.md +76 -0
- package/docs/guides/async-handlers.md +42 -0
- package/docs/guides/dependencies.md +153 -0
- package/docs/guides/filtering.md +153 -0
- package/docs/guides/handlers.md +236 -0
- package/docs/guides/llm-handlers.md +145 -0
- package/docs/guides/manifest.md +237 -0
- package/docs/guides/short-circuit.md +31 -0
- package/docs/guides/system-service.md +62 -0
- package/docs/index.html +43 -0
- package/docs/index.md +96 -0
- package/docs/operations/architecture.md +105 -0
- package/docs/operations/monitoring.md +94 -0
- package/docs/operations/security.md +76 -0
- package/docs/operations/troubleshooting.md +123 -0
- package/docs/plugins/cc-plugin-import.md +55 -0
- package/docs/plugins/creating-plugins.md +144 -0
- package/docs/plugins/using-plugins.md +63 -0
- package/docs/reference/cli.md +213 -0
- package/docs/reference/config-files.md +129 -0
- package/docs/reference/hook-events.md +128 -0
- package/docs/reference/http-api.md +122 -0
- package/docs/reference/types.md +410 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ Persistent hook runtime for Claude Code. Eliminate cold starts. Get observabilit
|
|
|
5
5
|
[](https://www.npmjs.com/package/@mauribadnights/clooks)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
**[Documentation](https://mauribadnights.github.io/clooks/)**
|
|
9
|
+
|
|
8
10
|
## Performance
|
|
9
11
|
|
|
10
12
|
| Metric | Without clooks | With clooks | Improvement |
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ const program = new commander_1.Command();
|
|
|
22
22
|
program
|
|
23
23
|
.name('clooks')
|
|
24
24
|
.description('Persistent hook runtime for Claude Code')
|
|
25
|
-
.version('0.5.
|
|
25
|
+
.version('0.5.1');
|
|
26
26
|
// --- start ---
|
|
27
27
|
program
|
|
28
28
|
.command('start')
|
|
@@ -32,11 +32,13 @@ program
|
|
|
32
32
|
.action(async (opts) => {
|
|
33
33
|
const noWatch = opts.watch === false;
|
|
34
34
|
if (!opts.foreground) {
|
|
35
|
-
//
|
|
35
|
+
// Fix 6: Idempotent start — check if daemon is already healthy first
|
|
36
|
+
// This covers both normal PID file case AND orphaned daemon (no PID file)
|
|
36
37
|
if ((0, server_js_1.isDaemonRunning)()) {
|
|
37
38
|
const healthy = await (0, server_js_1.isDaemonHealthy)();
|
|
38
39
|
if (healthy) {
|
|
39
|
-
|
|
40
|
+
const pid = (0, fs_1.existsSync)(constants_js_1.PID_FILE) ? (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim() : '?';
|
|
41
|
+
console.log(`Daemon is already running (pid ${pid}).`);
|
|
40
42
|
process.exit(0);
|
|
41
43
|
}
|
|
42
44
|
// PID alive but daemon unhealthy — stale process after sleep/lid-close
|
|
@@ -45,6 +47,16 @@ program
|
|
|
45
47
|
console.log(`Cleaned up stale daemon (pid ${stalePid}), starting fresh`);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
else {
|
|
51
|
+
// No PID file or dead PID — check if an orphaned daemon is on the port
|
|
52
|
+
const health = await (0, server_js_1.probeHealth)();
|
|
53
|
+
if (health && health.pid) {
|
|
54
|
+
// Orphaned daemon found — re-adopt it
|
|
55
|
+
(0, server_js_1.writePidFile)(health.pid);
|
|
56
|
+
console.log(`Adopted existing daemon (pid ${health.pid}).`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
48
60
|
// Ensure config dir exists
|
|
49
61
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
50
62
|
(0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
|
|
@@ -63,7 +75,15 @@ program
|
|
|
63
75
|
console.log(`Daemon started (pid ${pid}), listening on 127.0.0.1:${constants_js_1.DEFAULT_PORT}`);
|
|
64
76
|
}
|
|
65
77
|
else {
|
|
66
|
-
|
|
78
|
+
// Fix 1: After spawn, if PID file missing, check if port responded (orphan recovery)
|
|
79
|
+
const health = await (0, server_js_1.probeHealth)();
|
|
80
|
+
if (health && health.pid) {
|
|
81
|
+
(0, server_js_1.writePidFile)(health.pid);
|
|
82
|
+
console.log(`Adopted existing daemon (pid ${health.pid}).`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log('Daemon started. Check ~/.clooks/daemon.log if issues arise.');
|
|
86
|
+
}
|
|
67
87
|
}
|
|
68
88
|
process.exit(0);
|
|
69
89
|
}
|
|
@@ -86,12 +106,24 @@ program
|
|
|
86
106
|
program
|
|
87
107
|
.command('stop')
|
|
88
108
|
.description('Stop the clooks daemon')
|
|
89
|
-
.action(() => {
|
|
109
|
+
.action(async () => {
|
|
110
|
+
// Try sync stop first (fast path with PID file)
|
|
90
111
|
if ((0, server_js_1.stopDaemon)()) {
|
|
91
112
|
console.log('Daemon stopped.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Fix 4: No PID file — try async recovery via /health
|
|
116
|
+
const result = await (0, server_js_1.stopDaemonAsync)();
|
|
117
|
+
if (result.stopped) {
|
|
118
|
+
if (result.recovered) {
|
|
119
|
+
console.log(`Daemon stopped (recovered orphan, pid ${result.pid}).`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log('Daemon stopped.');
|
|
123
|
+
}
|
|
92
124
|
}
|
|
93
125
|
else {
|
|
94
|
-
console.log('Daemon is not running
|
|
126
|
+
console.log('Daemon is not running.');
|
|
95
127
|
}
|
|
96
128
|
});
|
|
97
129
|
// --- status ---
|
|
@@ -100,18 +132,28 @@ program
|
|
|
100
132
|
.description('Show daemon status')
|
|
101
133
|
.action(async () => {
|
|
102
134
|
const running = (0, server_js_1.isDaemonRunning)();
|
|
135
|
+
const serviceStatus = (0, service_js_1.getServiceStatus)();
|
|
103
136
|
if (!running) {
|
|
137
|
+
// Fix 2: No PID file or dead PID — check if orphaned daemon is on the port
|
|
138
|
+
const health = await (0, server_js_1.probeHealth)();
|
|
139
|
+
if (health && health.pid) {
|
|
140
|
+
// Orphaned daemon found — recover PID file
|
|
141
|
+
(0, server_js_1.writePidFile)(health.pid);
|
|
142
|
+
console.log(`Status: running (recovered, pid ${health.pid})`);
|
|
143
|
+
console.log(`Port: ${constants_js_1.DEFAULT_PORT}`);
|
|
144
|
+
console.log(`Service: ${serviceStatus}`);
|
|
145
|
+
console.log('Note: PID file was missing. Re-adopted orphaned daemon.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
104
148
|
console.log('Status: stopped');
|
|
105
149
|
return;
|
|
106
150
|
}
|
|
107
151
|
const pid = (0, fs_1.existsSync)(constants_js_1.PID_FILE) ? (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim() : '?';
|
|
108
|
-
// Try to hit health endpoint
|
|
109
|
-
// Service status
|
|
110
|
-
const serviceStatus = (0, service_js_1.getServiceStatus)();
|
|
152
|
+
// Try to hit health endpoint for detailed info
|
|
111
153
|
try {
|
|
112
154
|
const { get } = await import('http');
|
|
113
155
|
const data = await new Promise((resolve, reject) => {
|
|
114
|
-
const req = get(`http://127.0.0.1:${constants_js_1.DEFAULT_PORT}/health`, (res) => {
|
|
156
|
+
const req = get(`http://127.0.0.1:${constants_js_1.DEFAULT_PORT}/health/detail`, (res) => {
|
|
115
157
|
let body = '';
|
|
116
158
|
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
117
159
|
res.on('end', () => resolve(body));
|
|
@@ -333,6 +375,15 @@ program
|
|
|
333
375
|
}
|
|
334
376
|
}
|
|
335
377
|
}
|
|
378
|
+
else {
|
|
379
|
+
// No PID file — check for orphaned daemon on the port
|
|
380
|
+
const health = await (0, server_js_1.probeHealth)();
|
|
381
|
+
if (health && health.pid) {
|
|
382
|
+
(0, server_js_1.writePidFile)(health.pid);
|
|
383
|
+
(0, sync_js_1.syncSettings)();
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
336
387
|
// Ensure config dir exists
|
|
337
388
|
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
338
389
|
(0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput, PrefetchContext } from './types.js';
|
|
2
|
+
/** Reset cached shell env (for testing) */
|
|
3
|
+
export declare function resetShellEnv(): void;
|
|
2
4
|
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
3
5
|
declare function matchAgent(pattern: string, currentAgent: string): boolean;
|
|
4
6
|
/** Match handler project field against cwd path */
|
package/dist/handlers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// clooks hook handlers — execution engine
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.resetShellEnv = resetShellEnv;
|
|
4
5
|
exports.matchAgent = matchAgent;
|
|
5
6
|
exports.matchProject = matchProject;
|
|
6
7
|
exports.resetHandlerStates = resetHandlerStates;
|
|
@@ -17,6 +18,47 @@ const constants_js_1 = require("./constants.js");
|
|
|
17
18
|
const filter_js_1 = require("./filter.js");
|
|
18
19
|
const llm_js_1 = require("./llm.js");
|
|
19
20
|
const deps_js_1 = require("./deps.js");
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the user's login shell PATH once at startup.
|
|
23
|
+
* When the daemon runs under launchd/systemd, it inherits a minimal PATH
|
|
24
|
+
* that may not include /opt/homebrew/bin, pyenv shims, nvm dirs, etc.
|
|
25
|
+
* This ensures script handlers see the same PATH as the user's terminal.
|
|
26
|
+
*/
|
|
27
|
+
let _shellEnv = null;
|
|
28
|
+
function getShellEnv() {
|
|
29
|
+
if (_shellEnv)
|
|
30
|
+
return _shellEnv;
|
|
31
|
+
try {
|
|
32
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
33
|
+
const output = (0, child_process_1.execSync)(`${shell} -ilc 'env'`, {
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
const env = {};
|
|
39
|
+
for (const line of output.split('\n')) {
|
|
40
|
+
const idx = line.indexOf('=');
|
|
41
|
+
if (idx > 0) {
|
|
42
|
+
env[line.slice(0, idx)] = line.slice(idx + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Merge: shell env as base, but keep daemon-specific vars (like ANTHROPIC_API_KEY)
|
|
46
|
+
_shellEnv = { ...env, ...process.env };
|
|
47
|
+
// PATH specifically: prefer the shell's PATH (has homebrew, pyenv, nvm, etc.)
|
|
48
|
+
if (env.PATH) {
|
|
49
|
+
_shellEnv.PATH = env.PATH;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Fallback: just use process.env as-is
|
|
54
|
+
_shellEnv = process.env;
|
|
55
|
+
}
|
|
56
|
+
return _shellEnv;
|
|
57
|
+
}
|
|
58
|
+
/** Reset cached shell env (for testing) */
|
|
59
|
+
function resetShellEnv() {
|
|
60
|
+
_shellEnv = null;
|
|
61
|
+
}
|
|
20
62
|
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
21
63
|
function matchAgent(pattern, currentAgent) {
|
|
22
64
|
const agents = pattern.split(',').map(a => a.trim().toLowerCase());
|
|
@@ -313,6 +355,7 @@ function executeScriptHandler(handler, input) {
|
|
|
313
355
|
const child = (0, child_process_1.spawn)('sh', ['-c', h.command], {
|
|
314
356
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
315
357
|
timeout,
|
|
358
|
+
env: getShellEnv(),
|
|
316
359
|
});
|
|
317
360
|
let stdout = '';
|
|
318
361
|
let stderr = '';
|
package/dist/server.d.ts
CHANGED
|
@@ -33,8 +33,18 @@ export declare function startDaemon(manifest: Manifest, metrics: MetricsCollecto
|
|
|
33
33
|
}): Promise<ServerContext>;
|
|
34
34
|
/**
|
|
35
35
|
* Stop a running daemon by reading PID file and sending SIGTERM.
|
|
36
|
+
* If no PID file exists, tries to recover PID from the health endpoint.
|
|
36
37
|
*/
|
|
37
38
|
export declare function stopDaemon(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Async version of stopDaemon that can recover orphaned daemons via /health.
|
|
41
|
+
* Returns { stopped: boolean, pid?: number, recovered?: boolean }.
|
|
42
|
+
*/
|
|
43
|
+
export declare function stopDaemonAsync(): Promise<{
|
|
44
|
+
stopped: boolean;
|
|
45
|
+
pid?: number;
|
|
46
|
+
recovered?: boolean;
|
|
47
|
+
}>;
|
|
38
48
|
/**
|
|
39
49
|
* Check if daemon is currently running (PID check only).
|
|
40
50
|
* Use for stop/status where a quick check is fine.
|
|
@@ -46,6 +56,18 @@ export declare function isDaemonRunning(): boolean;
|
|
|
46
56
|
* Use for ensure-running and start where correctness matters.
|
|
47
57
|
*/
|
|
48
58
|
export declare function isDaemonHealthy(): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Probe the health endpoint on the daemon port.
|
|
61
|
+
* Returns the parsed health response (including pid) or null if unreachable.
|
|
62
|
+
*/
|
|
63
|
+
export declare function probeHealth(port?: number): Promise<{
|
|
64
|
+
status: string;
|
|
65
|
+
pid: number;
|
|
66
|
+
} | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Write a PID to the daemon PID file, creating the config dir if needed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function writePidFile(pid: number): void;
|
|
49
71
|
/**
|
|
50
72
|
* Clean up a stale daemon: remove PID file and attempt to kill the process.
|
|
51
73
|
* Returns the stale PID for logging purposes.
|
package/dist/server.js
CHANGED
|
@@ -5,8 +5,11 @@ exports.sessionAgents = void 0;
|
|
|
5
5
|
exports.createServer = createServer;
|
|
6
6
|
exports.startDaemon = startDaemon;
|
|
7
7
|
exports.stopDaemon = stopDaemon;
|
|
8
|
+
exports.stopDaemonAsync = stopDaemonAsync;
|
|
8
9
|
exports.isDaemonRunning = isDaemonRunning;
|
|
9
10
|
exports.isDaemonHealthy = isDaemonHealthy;
|
|
11
|
+
exports.probeHealth = probeHealth;
|
|
12
|
+
exports.writePidFile = writePidFile;
|
|
10
13
|
exports.cleanupStaleDaemon = cleanupStaleDaemon;
|
|
11
14
|
exports.startDaemonBackground = startDaemonBackground;
|
|
12
15
|
const http_1 = require("http");
|
|
@@ -123,9 +126,9 @@ function createServer(manifest, metrics) {
|
|
|
123
126
|
const server = (0, http_1.createServer)(async (req, res) => {
|
|
124
127
|
const url = req.url ?? '/';
|
|
125
128
|
const method = req.method ?? 'GET';
|
|
126
|
-
// Public health endpoint —
|
|
129
|
+
// Public health endpoint — includes PID for orphan recovery
|
|
127
130
|
if (method === 'GET' && url === '/health') {
|
|
128
|
-
sendJson(res, 200, { status: 'ok' });
|
|
131
|
+
sendJson(res, 200, { status: 'ok', pid: process.pid });
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
134
|
// Detailed health endpoint — authenticated if authToken configured
|
|
@@ -310,10 +313,23 @@ function startDaemon(manifest, metrics, options) {
|
|
|
310
313
|
return new Promise((resolve, reject) => {
|
|
311
314
|
const ctx = createServer(manifest, metrics);
|
|
312
315
|
const port = manifest.settings?.port ?? constants_js_1.DEFAULT_PORT;
|
|
313
|
-
ctx.server.on('error', (err) => {
|
|
316
|
+
ctx.server.on('error', async (err) => {
|
|
314
317
|
if (err.code === 'EADDRINUSE') {
|
|
315
|
-
log(`Port ${port} already in use`);
|
|
316
|
-
|
|
318
|
+
log(`Port ${port} already in use — attempting orphan recovery`);
|
|
319
|
+
// Try to recover: if a clooks daemon is already on this port, re-adopt it
|
|
320
|
+
try {
|
|
321
|
+
const health = await probeHealth(port);
|
|
322
|
+
if (health && health.pid) {
|
|
323
|
+
writePidFile(health.pid);
|
|
324
|
+
log(`Re-adopted orphaned daemon (pid ${health.pid})`);
|
|
325
|
+
resolve(ctx);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Probe failed — port is in use by something else
|
|
331
|
+
}
|
|
332
|
+
reject(new Error(`Port ${port} is already in use. Run 'clooks stop' to stop the existing daemon, or 'clooks status' to check.`));
|
|
317
333
|
}
|
|
318
334
|
else {
|
|
319
335
|
log(`Server error: ${err.message}`);
|
|
@@ -425,15 +441,21 @@ function startDaemon(manifest, metrics, options) {
|
|
|
425
441
|
}
|
|
426
442
|
/**
|
|
427
443
|
* Stop a running daemon by reading PID file and sending SIGTERM.
|
|
444
|
+
* If no PID file exists, tries to recover PID from the health endpoint.
|
|
428
445
|
*/
|
|
429
446
|
function stopDaemon() {
|
|
430
|
-
|
|
431
|
-
|
|
447
|
+
let pid = null;
|
|
448
|
+
if ((0, fs_1.existsSync)(constants_js_1.PID_FILE)) {
|
|
449
|
+
const pidStr = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
|
|
450
|
+
pid = parseInt(pidStr, 10);
|
|
451
|
+
if (isNaN(pid)) {
|
|
452
|
+
(0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
|
|
453
|
+
pid = null;
|
|
454
|
+
}
|
|
432
455
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
(0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
|
|
456
|
+
// If no PID from file, try synchronous recovery via health endpoint
|
|
457
|
+
// We can't await here (sync function), so use stopDaemonAsync for full recovery
|
|
458
|
+
if (pid === null) {
|
|
437
459
|
return false;
|
|
438
460
|
}
|
|
439
461
|
try {
|
|
@@ -462,6 +484,57 @@ function stopDaemon() {
|
|
|
462
484
|
}, 2000);
|
|
463
485
|
return true;
|
|
464
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Async version of stopDaemon that can recover orphaned daemons via /health.
|
|
489
|
+
* Returns { stopped: boolean, pid?: number, recovered?: boolean }.
|
|
490
|
+
*/
|
|
491
|
+
async function stopDaemonAsync() {
|
|
492
|
+
let pid = null;
|
|
493
|
+
if ((0, fs_1.existsSync)(constants_js_1.PID_FILE)) {
|
|
494
|
+
const pidStr = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
|
|
495
|
+
pid = parseInt(pidStr, 10);
|
|
496
|
+
if (isNaN(pid)) {
|
|
497
|
+
(0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
|
|
498
|
+
pid = null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// No PID file — try health endpoint recovery
|
|
502
|
+
if (pid === null) {
|
|
503
|
+
const health = await probeHealth();
|
|
504
|
+
if (health && health.pid) {
|
|
505
|
+
pid = health.pid;
|
|
506
|
+
log(`Recovered orphaned daemon PID ${pid} from /health for stop`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (pid === null) {
|
|
510
|
+
return { stopped: false };
|
|
511
|
+
}
|
|
512
|
+
const recovered = !(0, fs_1.existsSync)(constants_js_1.PID_FILE);
|
|
513
|
+
try {
|
|
514
|
+
process.kill(pid, 'SIGTERM');
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
try {
|
|
518
|
+
if ((0, fs_1.existsSync)(constants_js_1.PID_FILE))
|
|
519
|
+
(0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// ignore
|
|
523
|
+
}
|
|
524
|
+
return { stopped: false };
|
|
525
|
+
}
|
|
526
|
+
// Clean up PID file after a moment
|
|
527
|
+
setTimeout(() => {
|
|
528
|
+
try {
|
|
529
|
+
if ((0, fs_1.existsSync)(constants_js_1.PID_FILE))
|
|
530
|
+
(0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
// ignore
|
|
534
|
+
}
|
|
535
|
+
}, 2000);
|
|
536
|
+
return { stopped: true, pid, recovered };
|
|
537
|
+
}
|
|
465
538
|
/**
|
|
466
539
|
* Check if daemon is currently running (PID check only).
|
|
467
540
|
* Use for stop/status where a quick check is fine.
|
|
@@ -521,6 +594,42 @@ async function isDaemonHealthy() {
|
|
|
521
594
|
return false;
|
|
522
595
|
}
|
|
523
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* Probe the health endpoint on the daemon port.
|
|
599
|
+
* Returns the parsed health response (including pid) or null if unreachable.
|
|
600
|
+
*/
|
|
601
|
+
async function probeHealth(port) {
|
|
602
|
+
const p = port ?? constants_js_1.DEFAULT_PORT;
|
|
603
|
+
try {
|
|
604
|
+
const { get } = await import('http');
|
|
605
|
+
const data = await new Promise((resolve, reject) => {
|
|
606
|
+
const req = get(`http://127.0.0.1:${p}/health`, (res) => {
|
|
607
|
+
let body = '';
|
|
608
|
+
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
609
|
+
res.on('end', () => resolve(body));
|
|
610
|
+
});
|
|
611
|
+
req.on('error', reject);
|
|
612
|
+
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
613
|
+
});
|
|
614
|
+
const health = JSON.parse(data);
|
|
615
|
+
if (health.status === 'ok' && typeof health.pid === 'number') {
|
|
616
|
+
return health;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Write a PID to the daemon PID file, creating the config dir if needed.
|
|
626
|
+
*/
|
|
627
|
+
function writePidFile(pid) {
|
|
628
|
+
if (!(0, fs_1.existsSync)(constants_js_1.CONFIG_DIR)) {
|
|
629
|
+
(0, fs_1.mkdirSync)(constants_js_1.CONFIG_DIR, { recursive: true });
|
|
630
|
+
}
|
|
631
|
+
(0, fs_1.writeFileSync)(constants_js_1.PID_FILE, String(pid), 'utf-8');
|
|
632
|
+
}
|
|
524
633
|
/**
|
|
525
634
|
* Clean up a stale daemon: remove PID file and attempt to kill the process.
|
|
526
635
|
* Returns the stale PID for logging purposes.
|
package/docs/.nojekyll
ADDED
|
File without changes
|
package/docs/_sidebar.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
- **Getting Started**
|
|
2
|
+
- [Installation](getting-started/installation.md)
|
|
3
|
+
- [Quickstart](getting-started/quickstart.md)
|
|
4
|
+
- [Migration](getting-started/migration.md)
|
|
5
|
+
|
|
6
|
+
- **Guides**
|
|
7
|
+
- [Manifest](guides/manifest.md)
|
|
8
|
+
- [Handlers](guides/handlers.md)
|
|
9
|
+
- [LLM Handlers](guides/llm-handlers.md)
|
|
10
|
+
- [Filtering](guides/filtering.md)
|
|
11
|
+
- [Dependencies](guides/dependencies.md)
|
|
12
|
+
- [Async Handlers](guides/async-handlers.md)
|
|
13
|
+
- [Short-Circuit](guides/short-circuit.md)
|
|
14
|
+
- [System Service](guides/system-service.md)
|
|
15
|
+
|
|
16
|
+
- **Plugins**
|
|
17
|
+
- [Using Plugins](plugins/using-plugins.md)
|
|
18
|
+
- [Creating Plugins](plugins/creating-plugins.md)
|
|
19
|
+
- [CC Plugin Import](plugins/cc-plugin-import.md)
|
|
20
|
+
|
|
21
|
+
- **Reference**
|
|
22
|
+
- [CLI](reference/cli.md)
|
|
23
|
+
- [Hook Events](reference/hook-events.md)
|
|
24
|
+
- [HTTP API](reference/http-api.md)
|
|
25
|
+
- [Config Files](reference/config-files.md)
|
|
26
|
+
- [Types](reference/types.md)
|
|
27
|
+
|
|
28
|
+
- **Operations**
|
|
29
|
+
- [Monitoring](operations/monitoring.md)
|
|
30
|
+
- [Security](operations/security.md)
|
|
31
|
+
- [Troubleshooting](operations/troubleshooting.md)
|
|
32
|
+
- [Architecture](operations/architecture.md)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- **Node.js 18+** -- check with `node --version`
|
|
6
|
+
- **Claude Code** -- installed and working (`claude --version`)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @mauribadnights/clooks
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Initialize
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
clooks init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates your configuration directory and everything clooks needs to run. No existing hooks are modified -- use `clooks migrate` for that (see [Migration](migration.md)).
|
|
21
|
+
|
|
22
|
+
## Verify
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
clooks doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Doctor runs health checks on the daemon, port, manifest, settings, and handler state. A passing report means clooks is ready.
|
|
29
|
+
|
|
30
|
+
## What `clooks init` creates
|
|
31
|
+
|
|
32
|
+
| Item | Path | Description |
|
|
33
|
+
|------|------|-------------|
|
|
34
|
+
| Manifest | `~/.clooks/manifest.yaml` | Handler definitions, settings, and prefetch config |
|
|
35
|
+
| Hooks directory | `~/.clooks/hooks/` | Built-in hook scripts |
|
|
36
|
+
| Auth token | Stored in manifest | Generated once and shown at init. Used to authenticate requests to the daemon. |
|
|
37
|
+
| System service | launchd (macOS) / systemd (Linux) | Auto-starts the daemon on login, restarts on crash |
|
|
38
|
+
| Expert agent | `~/.claude/agents/clooks.md` | Invoke with `claude --agent clooks` for clooks-specific help |
|
|
39
|
+
|
|
40
|
+
> **Note:** The auth token is displayed once during init. It is stored in your manifest under `settings.authToken`. If you lose it, run `clooks rotate-token` to generate a new one.
|
|
41
|
+
|
|
42
|
+
## Next steps
|
|
43
|
+
|
|
44
|
+
Start the daemon and add your first handler:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
clooks start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
[Home](../index.md) | Next: [Quickstart](quickstart.md)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Migration
|
|
2
|
+
|
|
3
|
+
Migrate existing Claude Code command hooks to clooks in one command.
|
|
4
|
+
|
|
5
|
+
## What migration does
|
|
6
|
+
|
|
7
|
+
`clooks migrate` converts your `settings.json` command hooks into clooks HTTP hooks backed by the daemon, with equivalent handlers defined in the manifest. Your original command hooks continue to work -- they just route through clooks instead of spawning processes directly.
|
|
8
|
+
|
|
9
|
+
## Run the migration
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
clooks migrate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Step-by-step breakdown
|
|
16
|
+
|
|
17
|
+
When you run `clooks migrate`, the following happens in order:
|
|
18
|
+
|
|
19
|
+
1. **Reads settings** -- Loads `~/.claude/settings.json` (or `settings.local.json` if present).
|
|
20
|
+
2. **Backs up original** -- Saves a copy to `~/.clooks/settings.backup.json`.
|
|
21
|
+
3. **Extracts command hooks** -- Parses all command hooks from the settings file and creates corresponding handlers in `~/.clooks/manifest.yaml`.
|
|
22
|
+
4. **Rewrites settings** -- Replaces command hooks with HTTP hooks pointing to `http://localhost:7890`.
|
|
23
|
+
5. **Adds ensure-running** -- Injects a `clooks ensure-running` command into `SessionStart` so the daemon auto-starts when Claude Code launches.
|
|
24
|
+
6. **Imports plugin hooks** -- Detects and imports any Claude Code plugin hooks into the manifest.
|
|
25
|
+
7. **Installs system service** -- Sets up launchd (macOS) or systemd (Linux) for auto-start on login and crash recovery.
|
|
26
|
+
|
|
27
|
+
## Verify
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
clooks doctor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A passing report confirms that the daemon is running, the manifest is valid, and `settings.json` points to clooks.
|
|
34
|
+
|
|
35
|
+
## Rollback
|
|
36
|
+
|
|
37
|
+
If anything goes wrong, restore your original settings:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
clooks restore
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This replaces `settings.json` with the backup created during migration. Your command hooks return to their original state.
|
|
44
|
+
|
|
45
|
+
## Keeping settings in sync
|
|
46
|
+
|
|
47
|
+
After migration, if you add new handlers to the manifest, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
clooks sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This updates `settings.json` to include HTTP hook entries for any new events in your manifest. You do not need to edit `settings.json` manually.
|
|
54
|
+
|
|
55
|
+
> **Note:** `clooks sync` only adds missing entries. It never removes or modifies existing hooks in `settings.json`.
|
|
56
|
+
|
|
57
|
+
## Summary
|
|
58
|
+
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `clooks migrate` | Full migration: backup, convert, rewrite, install service |
|
|
62
|
+
| `clooks restore` | Rollback to pre-migration settings.json |
|
|
63
|
+
| `clooks sync` | Add missing HTTP hook entries for new manifest events |
|
|
64
|
+
| `clooks doctor` | Verify everything is wired up correctly |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
[Home](../index.md) | Prev: [Quickstart](quickstart.md) | Next: [Manifest Guide](../guides/manifest.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Get your first clooks handler running in 5 minutes.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @mauribadnights/clooks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Initialize
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
clooks init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 3. Start the daemon
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
clooks start
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 4. Check status
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
clooks status
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You should see the daemon running on port 7890 with zero handlers loaded.
|
|
30
|
+
|
|
31
|
+
## 5. Add a handler
|
|
32
|
+
|
|
33
|
+
Open `~/.clooks/manifest.yaml` in your editor and add a handler under `PreToolUse`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
handlers:
|
|
37
|
+
PreToolUse:
|
|
38
|
+
- id: bash-logger
|
|
39
|
+
type: script
|
|
40
|
+
command: "echo '{\"additionalContext\": \"Reviewed by clooks\"}'"
|
|
41
|
+
filter: "Bash"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This handler fires before every Bash tool call and injects a note into Claude's context.
|
|
45
|
+
|
|
46
|
+
## 6. Hot-reload
|
|
47
|
+
|
|
48
|
+
Save the file. The daemon watches `manifest.yaml` and hot-reloads on change -- no restart needed.
|
|
49
|
+
|
|
50
|
+
You can confirm the reload in the daemon log:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
tail -1 ~/.clooks/daemon.log
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 7. Try it
|
|
57
|
+
|
|
58
|
+
Open Claude Code and run any Bash command. The handler fires automatically. You will see "Reviewed by clooks" appear in the context.
|
|
59
|
+
|
|
60
|
+
## 8. Check metrics
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
clooks stats
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This launches an interactive TUI showing execution counts, latency, and errors per event. Use `-t` for plain text output.
|
|
67
|
+
|
|
68
|
+
## What to try next
|
|
69
|
+
|
|
70
|
+
- Add a `filter` to scope handlers to specific tools
|
|
71
|
+
- Try an `llm` handler for AI-powered review
|
|
72
|
+
- Run `clooks migrate` to convert existing command hooks
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
[Home](../index.md) | Prev: [Installation](installation.md) | Next: [Migration](migration.md)
|