@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.
@@ -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`
@@ -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.3.2');
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, then spawn detached
34
+ // Background mode: check if already running and healthy
32
35
  if ((0, server_js_1.isDaemonRunning)()) {
33
- console.log('Daemon is already running.');
34
- process.exit(0);
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
- .action(() => {
127
- const metrics = new metrics_js_1.MetricsCollector();
128
- console.log(metrics.formatStatsTable());
129
- console.log('');
130
- console.log('Per Handler:');
131
- console.log(metrics.formatHandlerStatsTable());
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(metrics.formatCostTable());
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
- // Already running sync settings silently and exit fast
220
- (0, sync_js_1.syncSettings)();
221
- process.exit(0);
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
+ }
@@ -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)(eligible);
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 = [eligible];
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.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = exports.LOG_FILE = exports.METRICS_FILE = 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.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 = 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");