@mauribadnights/clooks 0.2.2 → 0.3.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.
@@ -3,15 +3,18 @@ import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput,
3
3
  export declare function resetHandlerStates(): void;
4
4
  /** Get a copy of the handler states map */
5
5
  export declare function getHandlerStates(): Map<string, HandlerState>;
6
+ /** Clean up state for a specific handler ID (used during manifest reload diffs). */
7
+ export declare function cleanupHandlerState(handlerId: string): void;
6
8
  /**
7
9
  * Reset handler states for handlers that have sessionIsolation: true.
8
10
  * Called on SessionStart events.
9
11
  */
10
12
  export declare function resetSessionIsolatedHandlers(handlers: HandlerConfig[]): void;
11
13
  /**
12
- * Execute all handlers for an event in parallel.
13
- * Returns merged results array.
14
- * Optionally accepts pre-fetched context for LLM prompt rendering.
14
+ * Execute all handlers for an event, respecting dependency order.
15
+ * Handlers are grouped into "waves" via topological sort.
16
+ * Within each wave, handlers run in parallel.
17
+ * Outputs from previous waves are available to dependent handlers via _handlerOutputs.
15
18
  */
16
19
  export declare function executeHandlers(_event: HookEvent, input: HookInput, handlers: HandlerConfig[], context?: PrefetchContext): Promise<HandlerResult[]>;
17
20
  /**
package/dist/handlers.js CHANGED
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.resetHandlerStates = resetHandlerStates;
5
5
  exports.getHandlerStates = getHandlerStates;
6
+ exports.cleanupHandlerState = cleanupHandlerState;
6
7
  exports.resetSessionIsolatedHandlers = resetSessionIsolatedHandlers;
7
8
  exports.executeHandlers = executeHandlers;
8
9
  exports.executeScriptHandler = executeScriptHandler;
@@ -13,6 +14,7 @@ const path_1 = require("path");
13
14
  const constants_js_1 = require("./constants.js");
14
15
  const filter_js_1 = require("./filter.js");
15
16
  const llm_js_1 = require("./llm.js");
17
+ const deps_js_1 = require("./deps.js");
16
18
  /** Runtime state per handler ID */
17
19
  const handlerStates = new Map();
18
20
  function getState(id) {
@@ -31,6 +33,10 @@ function resetHandlerStates() {
31
33
  function getHandlerStates() {
32
34
  return new Map(handlerStates);
33
35
  }
36
+ /** Clean up state for a specific handler ID (used during manifest reload diffs). */
37
+ function cleanupHandlerState(handlerId) {
38
+ handlerStates.delete(handlerId);
39
+ }
34
40
  /**
35
41
  * Reset handler states for handlers that have sessionIsolation: true.
36
42
  * Called on SessionStart events.
@@ -49,17 +55,16 @@ function resetSessionIsolatedHandlers(handlers) {
49
55
  }
50
56
  }
51
57
  /**
52
- * Execute all handlers for an event in parallel.
53
- * Returns merged results array.
54
- * Optionally accepts pre-fetched context for LLM prompt rendering.
58
+ * Execute all handlers for an event, respecting dependency order.
59
+ * Handlers are grouped into "waves" via topological sort.
60
+ * Within each wave, handlers run in parallel.
61
+ * Outputs from previous waves are available to dependent handlers via _handlerOutputs.
55
62
  */
56
63
  async function executeHandlers(_event, input, handlers, context) {
57
- // Separate LLM handlers from script/inline, applying shared pre-checks
58
- const llmHandlers = [];
59
- const otherPromises = [];
64
+ // Pre-check: filter out disabled/auto-disabled/filtered handlers before dep resolution
65
+ const eligible = [];
60
66
  const skippedResults = [];
61
67
  for (const handler of handlers) {
62
- // Skip disabled handlers (both manifest-disabled and auto-disabled)
63
68
  if (handler.enabled === false) {
64
69
  skippedResults.push({ id: handler.id, ok: true, output: undefined, duration_ms: 0 });
65
70
  continue;
@@ -74,7 +79,6 @@ async function executeHandlers(_event, input, handlers, context) {
74
79
  });
75
80
  continue;
76
81
  }
77
- // Evaluate keyword filter before execution
78
82
  if (handler.filter) {
79
83
  const inputStr = JSON.stringify(input);
80
84
  if (!(0, filter_js_1.evaluateFilter)(handler.filter, inputStr)) {
@@ -88,51 +92,82 @@ async function executeHandlers(_event, input, handlers, context) {
88
92
  continue;
89
93
  }
90
94
  }
91
- state.totalFires++;
92
- if (handler.type === 'llm') {
93
- llmHandlers.push(handler);
94
- }
95
- else {
96
- // Execute script/inline handlers in parallel
97
- otherPromises.push(executeOtherHandler(handler, input));
98
- }
95
+ eligible.push(handler);
96
+ }
97
+ if (eligible.length === 0) {
98
+ return skippedResults;
99
99
  }
100
- // Execute script/inline handlers in parallel
101
- const otherResults = otherPromises.length > 0
102
- ? await Promise.all(otherPromises)
103
- : [];
104
- // Execute LLM handlers with batching (graceful — never crashes)
105
- let llmResults = [];
106
- if (llmHandlers.length > 0) {
107
- try {
108
- llmResults = await (0, llm_js_1.executeLLMHandlersBatched)(llmHandlers, input, context ?? {});
100
+ // Resolve execution order into waves
101
+ let waves;
102
+ try {
103
+ waves = (0, deps_js_1.resolveExecutionOrder)(eligible);
104
+ }
105
+ catch {
106
+ // If dep resolution fails, fall back to flat parallel execution
107
+ waves = [eligible];
108
+ }
109
+ const allResults = [...skippedResults];
110
+ const handlerOutputs = {};
111
+ for (const wave of waves) {
112
+ // Mark totalFires for all handlers in this wave
113
+ for (const handler of wave) {
114
+ getState(handler.id).totalFires++;
109
115
  }
110
- catch (err) {
111
- // Graceful degradation: if LLM execution entirely fails, return error results
112
- const errorMsg = err instanceof Error ? err.message : String(err);
113
- llmResults = llmHandlers.map(h => ({
114
- id: h.id,
115
- ok: false,
116
- error: `LLM execution failed: ${errorMsg}`,
117
- duration_ms: 0,
118
- }));
116
+ // Build input with _handlerOutputs from previous waves
117
+ const waveInput = Object.keys(handlerOutputs).length > 0
118
+ ? { ...input, _handlerOutputs: handlerOutputs }
119
+ : input;
120
+ // Separate LLM from script/inline within this wave
121
+ const llmHandlers = [];
122
+ const otherPromises = [];
123
+ for (const handler of wave) {
124
+ if (handler.type === 'llm') {
125
+ llmHandlers.push(handler);
126
+ }
127
+ else {
128
+ otherPromises.push(executeOtherHandler(handler, waveInput));
129
+ }
119
130
  }
120
- }
121
- // Update failure tracking for all executed results
122
- for (const result of [...otherResults, ...llmResults]) {
123
- const state = getState(result.id);
124
- if (result.ok) {
125
- state.consecutiveFailures = 0;
131
+ // Execute script/inline handlers in parallel
132
+ const otherResults = otherPromises.length > 0
133
+ ? await Promise.all(otherPromises)
134
+ : [];
135
+ // Execute LLM handlers with batching (scoped to this wave)
136
+ let llmResults = [];
137
+ if (llmHandlers.length > 0) {
138
+ try {
139
+ llmResults = await (0, llm_js_1.executeLLMHandlersBatched)(llmHandlers, waveInput, context ?? {}, input.session_id);
140
+ }
141
+ catch (err) {
142
+ const errorMsg = err instanceof Error ? err.message : String(err);
143
+ llmResults = llmHandlers.map(h => ({
144
+ id: h.id,
145
+ ok: false,
146
+ error: `LLM execution failed: ${errorMsg}`,
147
+ duration_ms: 0,
148
+ }));
149
+ }
126
150
  }
127
- else {
128
- state.consecutiveFailures++;
129
- state.totalErrors++;
130
- if (state.consecutiveFailures >= constants_js_1.MAX_CONSECUTIVE_FAILURES) {
131
- state.disabled = true;
151
+ const waveResults = [...otherResults, ...llmResults];
152
+ // Update failure tracking and collect outputs for dependents
153
+ for (const result of waveResults) {
154
+ const state = getState(result.id);
155
+ if (result.ok) {
156
+ state.consecutiveFailures = 0;
157
+ }
158
+ else {
159
+ state.consecutiveFailures++;
160
+ state.totalErrors++;
161
+ if (state.consecutiveFailures >= constants_js_1.MAX_CONSECUTIVE_FAILURES) {
162
+ state.disabled = true;
163
+ }
132
164
  }
165
+ // Store output for downstream handlers
166
+ handlerOutputs[result.id] = result.output;
133
167
  }
168
+ allResults.push(...waveResults);
134
169
  }
135
- return [...skippedResults, ...otherResults, ...llmResults];
170
+ return allResults;
136
171
  }
137
172
  /**
138
173
  * Execute a single script or inline handler with error handling.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  export { createServer, startDaemon, stopDaemon, isDaemonRunning } from './server.js';
2
- export { loadManifest, validateManifest, createDefaultManifest } from './manifest.js';
2
+ export { loadManifest, loadCompositeManifest, validateManifest, createDefaultManifest } from './manifest.js';
3
+ export { loadPlugins, mergeManifests, validatePluginManifest, loadRegistry, saveRegistry, installPlugin, uninstallPlugin, listPlugins } from './plugin.js';
3
4
  export { MetricsCollector } from './metrics.js';
4
5
  export { migrate, restore, getSettingsPath } from './migrate.js';
5
6
  export type { MigratePathOptions } from './migrate.js';
6
7
  export { runDoctor } from './doctor.js';
7
- export { executeHandlers, resetSessionIsolatedHandlers } from './handlers.js';
8
+ export { executeHandlers, resetSessionIsolatedHandlers, cleanupHandlerState } from './handlers.js';
9
+ export { resolveExecutionOrder } from './deps.js';
10
+ export { DenyCache } from './shortcircuit.js';
11
+ export { RateLimiter } from './ratelimit.js';
8
12
  export { startWatcher, stopWatcher } from './watcher.js';
9
- export { generateAuthToken, validateAuth } from './auth.js';
13
+ export { generateAuthToken, validateAuth, rotateToken } from './auth.js';
14
+ export type { RotateTokenOptions } from './auth.js';
10
15
  export { evaluateFilter } from './filter.js';
11
16
  export { executeLLMHandler, executeLLMHandlersBatched, calculateCost, resetClient } from './llm.js';
12
17
  export { prefetchContext, renderPromptTemplate } from './prefetch.js';
13
- export { DEFAULT_PORT, CONFIG_DIR, MANIFEST_PATH, PID_FILE, METRICS_FILE, LOG_FILE, COSTS_FILE, DEFAULT_LLM_TIMEOUT, DEFAULT_LLM_MAX_TOKENS, LLM_PRICING } from './constants.js';
14
- export type { HookEvent, HookInput, HandlerType, HandlerConfig, ScriptHandlerConfig, InlineHandlerConfig, LLMHandlerConfig, LLMModel, Manifest, HandlerResult, MetricEntry, HandlerState, DiagnosticResult, PrefetchKey, PrefetchContext, TokenUsage, CostEntry, } from './types.js';
18
+ export { DEFAULT_PORT, CONFIG_DIR, MANIFEST_PATH, PID_FILE, METRICS_FILE, LOG_FILE, COSTS_FILE, DEFAULT_LLM_TIMEOUT, DEFAULT_LLM_MAX_TOKENS, LLM_PRICING, PLUGINS_DIR, PLUGIN_REGISTRY, PLUGIN_MANIFEST_NAME } from './constants.js';
19
+ export type { HookEvent, HookInput, HandlerType, HandlerConfig, ScriptHandlerConfig, InlineHandlerConfig, LLMHandlerConfig, LLMModel, Manifest, HandlerResult, MetricEntry, HandlerState, DiagnosticResult, PrefetchKey, PrefetchContext, TokenUsage, CostEntry, PluginManifest, InstalledPlugin, PluginRegistry, } from './types.js';
package/dist/index.js CHANGED
@@ -1,7 +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.validateAuth = exports.generateAuthToken = exports.stopWatcher = exports.startWatcher = exports.resetSessionIsolatedHandlers = exports.executeHandlers = exports.runDoctor = exports.getSettingsPath = exports.restore = exports.migrate = exports.MetricsCollector = exports.createDefaultManifest = exports.validateManifest = exports.loadManifest = exports.isDaemonRunning = exports.stopDaemon = exports.startDaemon = exports.createServer = void 0;
4
+ exports.PLUGINS_DIR = 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.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 = void 0;
5
6
  var server_js_1 = require("./server.js");
6
7
  Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
7
8
  Object.defineProperty(exports, "startDaemon", { enumerable: true, get: function () { return server_js_1.startDaemon; } });
@@ -9,8 +10,18 @@ Object.defineProperty(exports, "stopDaemon", { enumerable: true, get: function (
9
10
  Object.defineProperty(exports, "isDaemonRunning", { enumerable: true, get: function () { return server_js_1.isDaemonRunning; } });
10
11
  var manifest_js_1 = require("./manifest.js");
11
12
  Object.defineProperty(exports, "loadManifest", { enumerable: true, get: function () { return manifest_js_1.loadManifest; } });
13
+ Object.defineProperty(exports, "loadCompositeManifest", { enumerable: true, get: function () { return manifest_js_1.loadCompositeManifest; } });
12
14
  Object.defineProperty(exports, "validateManifest", { enumerable: true, get: function () { return manifest_js_1.validateManifest; } });
13
15
  Object.defineProperty(exports, "createDefaultManifest", { enumerable: true, get: function () { return manifest_js_1.createDefaultManifest; } });
16
+ var plugin_js_1 = require("./plugin.js");
17
+ Object.defineProperty(exports, "loadPlugins", { enumerable: true, get: function () { return plugin_js_1.loadPlugins; } });
18
+ Object.defineProperty(exports, "mergeManifests", { enumerable: true, get: function () { return plugin_js_1.mergeManifests; } });
19
+ Object.defineProperty(exports, "validatePluginManifest", { enumerable: true, get: function () { return plugin_js_1.validatePluginManifest; } });
20
+ Object.defineProperty(exports, "loadRegistry", { enumerable: true, get: function () { return plugin_js_1.loadRegistry; } });
21
+ Object.defineProperty(exports, "saveRegistry", { enumerable: true, get: function () { return plugin_js_1.saveRegistry; } });
22
+ Object.defineProperty(exports, "installPlugin", { enumerable: true, get: function () { return plugin_js_1.installPlugin; } });
23
+ Object.defineProperty(exports, "uninstallPlugin", { enumerable: true, get: function () { return plugin_js_1.uninstallPlugin; } });
24
+ Object.defineProperty(exports, "listPlugins", { enumerable: true, get: function () { return plugin_js_1.listPlugins; } });
14
25
  var metrics_js_1 = require("./metrics.js");
15
26
  Object.defineProperty(exports, "MetricsCollector", { enumerable: true, get: function () { return metrics_js_1.MetricsCollector; } });
16
27
  var migrate_js_1 = require("./migrate.js");
@@ -22,12 +33,20 @@ Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function ()
22
33
  var handlers_js_1 = require("./handlers.js");
23
34
  Object.defineProperty(exports, "executeHandlers", { enumerable: true, get: function () { return handlers_js_1.executeHandlers; } });
24
35
  Object.defineProperty(exports, "resetSessionIsolatedHandlers", { enumerable: true, get: function () { return handlers_js_1.resetSessionIsolatedHandlers; } });
36
+ Object.defineProperty(exports, "cleanupHandlerState", { enumerable: true, get: function () { return handlers_js_1.cleanupHandlerState; } });
37
+ var deps_js_1 = require("./deps.js");
38
+ Object.defineProperty(exports, "resolveExecutionOrder", { enumerable: true, get: function () { return deps_js_1.resolveExecutionOrder; } });
39
+ var shortcircuit_js_1 = require("./shortcircuit.js");
40
+ Object.defineProperty(exports, "DenyCache", { enumerable: true, get: function () { return shortcircuit_js_1.DenyCache; } });
41
+ var ratelimit_js_1 = require("./ratelimit.js");
42
+ Object.defineProperty(exports, "RateLimiter", { enumerable: true, get: function () { return ratelimit_js_1.RateLimiter; } });
25
43
  var watcher_js_1 = require("./watcher.js");
26
44
  Object.defineProperty(exports, "startWatcher", { enumerable: true, get: function () { return watcher_js_1.startWatcher; } });
27
45
  Object.defineProperty(exports, "stopWatcher", { enumerable: true, get: function () { return watcher_js_1.stopWatcher; } });
28
46
  var auth_js_1 = require("./auth.js");
29
47
  Object.defineProperty(exports, "generateAuthToken", { enumerable: true, get: function () { return auth_js_1.generateAuthToken; } });
30
48
  Object.defineProperty(exports, "validateAuth", { enumerable: true, get: function () { return auth_js_1.validateAuth; } });
49
+ Object.defineProperty(exports, "rotateToken", { enumerable: true, get: function () { return auth_js_1.rotateToken; } });
31
50
  var filter_js_1 = require("./filter.js");
32
51
  Object.defineProperty(exports, "evaluateFilter", { enumerable: true, get: function () { return filter_js_1.evaluateFilter; } });
33
52
  var llm_js_1 = require("./llm.js");
@@ -49,3 +68,6 @@ Object.defineProperty(exports, "COSTS_FILE", { enumerable: true, get: function (
49
68
  Object.defineProperty(exports, "DEFAULT_LLM_TIMEOUT", { enumerable: true, get: function () { return constants_js_1.DEFAULT_LLM_TIMEOUT; } });
50
69
  Object.defineProperty(exports, "DEFAULT_LLM_MAX_TOKENS", { enumerable: true, get: function () { return constants_js_1.DEFAULT_LLM_MAX_TOKENS; } });
51
70
  Object.defineProperty(exports, "LLM_PRICING", { enumerable: true, get: function () { return constants_js_1.LLM_PRICING; } });
71
+ Object.defineProperty(exports, "PLUGINS_DIR", { enumerable: true, get: function () { return constants_js_1.PLUGINS_DIR; } });
72
+ Object.defineProperty(exports, "PLUGIN_REGISTRY", { enumerable: true, get: function () { return constants_js_1.PLUGIN_REGISTRY; } });
73
+ Object.defineProperty(exports, "PLUGIN_MANIFEST_NAME", { enumerable: true, get: function () { return constants_js_1.PLUGIN_MANIFEST_NAME; } });
package/dist/llm.d.ts CHANGED
@@ -16,4 +16,4 @@ export declare function executeLLMHandler(handler: LLMHandlerConfig, input: Hook
16
16
  * a single API call with a structured multi-task prompt. Handlers without a
17
17
  * batchGroup are executed individually.
18
18
  */
19
- export declare function executeLLMHandlersBatched(handlers: LLMHandlerConfig[], input: HookInput, context: PrefetchContext): Promise<HandlerResult[]>;
19
+ export declare function executeLLMHandlersBatched(handlers: LLMHandlerConfig[], input: HookInput, context: PrefetchContext, sessionId?: string): Promise<HandlerResult[]>;
package/dist/llm.js CHANGED
@@ -190,15 +190,19 @@ function splitUsage(total, count) {
190
190
  * a single API call with a structured multi-task prompt. Handlers without a
191
191
  * batchGroup are executed individually.
192
192
  */
193
- async function executeLLMHandlersBatched(handlers, input, context) {
194
- // Group by batchGroup
193
+ async function executeLLMHandlersBatched(handlers, input, context, sessionId) {
194
+ // Group by batchGroup, scoped by sessionId to prevent cross-session batching
195
195
  const grouped = new Map();
196
196
  const ungrouped = [];
197
197
  for (const handler of handlers) {
198
198
  if (handler.batchGroup) {
199
- const existing = grouped.get(handler.batchGroup) ?? [];
199
+ // Scope the batch key by sessionId so different sessions never batch together
200
+ const batchKey = sessionId
201
+ ? `${handler.batchGroup}:${sessionId}`
202
+ : handler.batchGroup;
203
+ const existing = grouped.get(batchKey) ?? [];
200
204
  existing.push(handler);
201
- grouped.set(handler.batchGroup, existing);
205
+ grouped.set(batchKey, existing);
202
206
  }
203
207
  else {
204
208
  ungrouped.push(handler);
@@ -9,6 +9,10 @@ export declare function loadManifest(): Manifest;
9
9
  * Throws on invalid structure.
10
10
  */
11
11
  export declare function validateManifest(manifest: Manifest): void;
12
+ /**
13
+ * Load the composite manifest: user manifest + all installed plugins.
14
+ */
15
+ export declare function loadCompositeManifest(): Manifest;
12
16
  /**
13
17
  * Create a default commented example manifest.yaml in CONFIG_DIR.
14
18
  */
package/dist/manifest.js CHANGED
@@ -3,10 +3,14 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.loadManifest = loadManifest;
5
5
  exports.validateManifest = validateManifest;
6
+ exports.loadCompositeManifest = loadCompositeManifest;
6
7
  exports.createDefaultManifest = createDefaultManifest;
7
8
  const fs_1 = require("fs");
9
+ const path_1 = require("path");
8
10
  const yaml_1 = require("yaml");
9
11
  const constants_js_1 = require("./constants.js");
12
+ const plugin_js_1 = require("./plugin.js");
13
+ const builtin_hooks_js_1 = require("./builtin-hooks.js");
10
14
  /**
11
15
  * Load and validate the manifest from disk.
12
16
  * Returns a Manifest with empty handlers if the file doesn't exist.
@@ -99,6 +103,14 @@ function validateManifest(manifest) {
99
103
  }
100
104
  }
101
105
  }
106
+ /**
107
+ * Load the composite manifest: user manifest + all installed plugins.
108
+ */
109
+ function loadCompositeManifest() {
110
+ const userManifest = loadManifest();
111
+ const plugins = (0, plugin_js_1.loadPlugins)();
112
+ return (0, plugin_js_1.mergeManifests)(userManifest, plugins);
113
+ }
102
114
  /**
103
115
  * Create a default commented example manifest.yaml in CONFIG_DIR.
104
116
  */
@@ -113,8 +125,20 @@ function createDefaultManifest(authToken) {
113
125
  if (authToken) {
114
126
  settings.authToken = authToken;
115
127
  }
128
+ // Install built-in hook scripts to CONFIG_DIR/hooks/
129
+ (0, builtin_hooks_js_1.installBuiltinHooks)();
130
+ const checkUpdatePath = (0, path_1.join)(constants_js_1.HOOKS_DIR, 'check-update.js');
116
131
  const example = {
117
132
  handlers: {
133
+ SessionStart: [
134
+ {
135
+ id: 'clooks-check-update',
136
+ type: 'script',
137
+ command: `node ${checkUpdatePath}`,
138
+ timeout: 6000,
139
+ enabled: true,
140
+ },
141
+ ],
118
142
  PreToolUse: [
119
143
  {
120
144
  id: 'example-guard',
package/dist/metrics.d.ts CHANGED
@@ -7,6 +7,16 @@ interface AggregatedStats {
7
7
  minDuration: number;
8
8
  maxDuration: number;
9
9
  }
10
+ export interface HandlerStats {
11
+ handler: string;
12
+ event: string;
13
+ fires: number;
14
+ errors: number;
15
+ filtered: number;
16
+ avgDuration: number;
17
+ minDuration: number;
18
+ maxDuration: number;
19
+ }
10
20
  export declare class MetricsCollector {
11
21
  private static readonly MAX_ENTRIES;
12
22
  private entries;
@@ -22,6 +32,10 @@ export declare class MetricsCollector {
22
32
  getStats(): AggregatedStats[];
23
33
  /** Get stats for a specific session. */
24
34
  getSessionStats(sessionId: string): AggregatedStats[];
35
+ /** Get per-handler stats (not just per-event). */
36
+ getHandlerStats(): HandlerStats[];
37
+ /** Format per-handler stats as a CLI-friendly table. */
38
+ formatHandlerStatsTable(): string;
25
39
  /** Flush is a no-op since we append on every record, but provided for API completeness. */
26
40
  flush(): void;
27
41
  /** Format stats as a CLI-friendly table. */
package/dist/metrics.js CHANGED
@@ -95,6 +95,53 @@ class MetricsCollector {
95
95
  }
96
96
  return stats.sort((a, b) => b.fires - a.fires);
97
97
  }
98
+ /** Get per-handler stats (not just per-event). */
99
+ getHandlerStats() {
100
+ const all = this.loadAll();
101
+ const byHandler = new Map();
102
+ for (const entry of all) {
103
+ const existing = byHandler.get(entry.handler) ?? [];
104
+ existing.push(entry);
105
+ byHandler.set(entry.handler, existing);
106
+ }
107
+ const stats = [];
108
+ for (const [handler, entries] of byHandler) {
109
+ const durations = entries.map((e) => e.duration_ms);
110
+ stats.push({
111
+ handler,
112
+ event: entries[0].event,
113
+ fires: entries.length,
114
+ errors: entries.filter((e) => !e.ok).length,
115
+ filtered: entries.filter((e) => e.filtered).length,
116
+ avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
117
+ minDuration: Math.min(...durations),
118
+ maxDuration: Math.max(...durations),
119
+ });
120
+ }
121
+ return stats.sort((a, b) => {
122
+ if (b.fires !== a.fires)
123
+ return b.fires - a.fires;
124
+ return b.avgDuration - a.avgDuration;
125
+ });
126
+ }
127
+ /** Format per-handler stats as a CLI-friendly table. */
128
+ formatHandlerStatsTable() {
129
+ const stats = this.getHandlerStats();
130
+ if (stats.length === 0) {
131
+ return 'No per-handler metrics recorded yet.';
132
+ }
133
+ const header = padHandlerRow(['Handler', 'Event', 'Fires', 'Errors', 'Avg ms', 'Max ms']);
134
+ const separator = '-'.repeat(header.length);
135
+ const rows = stats.map((s) => padHandlerRow([
136
+ s.handler,
137
+ s.event,
138
+ String(s.fires),
139
+ String(s.errors),
140
+ s.avgDuration.toFixed(1),
141
+ s.maxDuration.toFixed(1),
142
+ ]));
143
+ return [header, separator, ...rows].join('\n');
144
+ }
98
145
  /** Flush is a no-op since we append on every record, but provided for API completeness. */
99
146
  flush() {
100
147
  // Already written on each record()
@@ -249,3 +296,7 @@ function padRow(cols) {
249
296
  const widths = [20, 8, 8, 10, 10, 10];
250
297
  return cols.map((col, i) => col.padEnd(widths[i])).join(' ');
251
298
  }
299
+ function padHandlerRow(cols) {
300
+ const widths = [35, 20, 7, 7, 8, 8];
301
+ return cols.map((col, i) => col.padEnd(widths[i])).join(' ');
302
+ }
package/dist/migrate.d.ts CHANGED
@@ -11,6 +11,15 @@ export interface MigratePathOptions {
11
11
  * Find the Claude Code settings.json path.
12
12
  */
13
13
  export declare function getSettingsPath(options?: MigratePathOptions): string | null;
14
+ /**
15
+ * Derive a readable handler ID from a hook command.
16
+ *
17
+ * "node /path/to/gsd-check-update.js" -> "gsd-check-update"
18
+ * "node /path/to/hooks/post-action.js" -> "post-action"
19
+ * "python3 -m almicio.hooks.session_context" -> "almicio-session-context"
20
+ * "bash -c 'source ~/.zshrc; python3 /path/to/tts-summary.py'" -> "tts-summary"
21
+ */
22
+ export declare function deriveHandlerId(command: string, event: string, index: number): string;
14
23
  /**
15
24
  * Migrate Claude Code settings.json command hooks to clooks HTTP hooks.
16
25
  *
package/dist/migrate.js CHANGED
@@ -2,12 +2,14 @@
2
2
  // clooks migration utilities — convert shell hooks to HTTP hooks
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.getSettingsPath = getSettingsPath;
5
+ exports.deriveHandlerId = deriveHandlerId;
5
6
  exports.migrate = migrate;
6
7
  exports.restore = restore;
7
8
  const fs_1 = require("fs");
8
9
  const path_1 = require("path");
9
10
  const os_1 = require("os");
10
11
  const constants_js_1 = require("./constants.js");
12
+ const builtin_hooks_js_1 = require("./builtin-hooks.js");
11
13
  const yaml_1 = require("yaml");
12
14
  /**
13
15
  * Find the Claude Code settings.json path.
@@ -25,6 +27,46 @@ function getSettingsPath(options) {
25
27
  }
26
28
  return null;
27
29
  }
30
+ /**
31
+ * Derive a readable handler ID from a hook command.
32
+ *
33
+ * "node /path/to/gsd-check-update.js" -> "gsd-check-update"
34
+ * "node /path/to/hooks/post-action.js" -> "post-action"
35
+ * "python3 -m almicio.hooks.session_context" -> "almicio-session-context"
36
+ * "bash -c 'source ~/.zshrc; python3 /path/to/tts-summary.py'" -> "tts-summary"
37
+ */
38
+ function deriveHandlerId(command, event, index) {
39
+ let basename = null;
40
+ // Try python3 -m module.name pattern
41
+ const moduleMatch = command.match(/python3?\s+-m\s+([\w.]+)/);
42
+ if (moduleMatch) {
43
+ const modulePath = moduleMatch[1];
44
+ const lastSegment = modulePath.split('.').pop() ?? '';
45
+ if (lastSegment)
46
+ basename = lastSegment;
47
+ }
48
+ // Try to find the last .js or .py file in the command
49
+ if (!basename) {
50
+ // Match quoted or unquoted file paths ending in .js or .py
51
+ const fileMatches = [...command.matchAll(/(?:["'])?([^\s"']+\.(?:js|py))(?:["'])?/g)];
52
+ if (fileMatches.length > 0) {
53
+ const lastFile = fileMatches[fileMatches.length - 1][1];
54
+ // Extract basename without extension
55
+ const parts = lastFile.split('/');
56
+ const filename = parts[parts.length - 1];
57
+ basename = filename.replace(/\.(js|py)$/, '');
58
+ }
59
+ }
60
+ if (!basename) {
61
+ return `migrated-${event.toLowerCase()}-${index}`;
62
+ }
63
+ // Sanitize: replace non-alphanumeric chars with hyphens, lowercase
64
+ let id = basename.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
65
+ if (!id) {
66
+ return `migrated-${event.toLowerCase()}-${index}`;
67
+ }
68
+ return id;
69
+ }
28
70
  /**
29
71
  * Migrate Claude Code settings.json command hooks to clooks HTTP hooks.
30
72
  *
@@ -60,6 +102,7 @@ function migrate(options) {
60
102
  // Extract command hooks and build manifest
61
103
  const manifestHandlers = {};
62
104
  let handlerIndex = 0;
105
+ const usedIds = new Set();
63
106
  // NOTE: In v0.1, matchers from the original rule groups are not preserved in the
64
107
  // migrated HTTP hooks — all command hooks are consolidated into matcher-less rule groups.
65
108
  // This is acceptable because clooks dispatches based on event type, not matchers.
@@ -82,8 +125,14 @@ function migrate(options) {
82
125
  continue;
83
126
  manifestHandlers[event] = commandHooks.map((hook) => {
84
127
  handlerIndex++;
128
+ let id = deriveHandlerId(hook.command, event, handlerIndex);
129
+ // Ensure uniqueness: if ID already used, append index
130
+ if (usedIds.has(id)) {
131
+ id = `${id}-${handlerIndex}`;
132
+ }
133
+ usedIds.add(id);
85
134
  return {
86
- id: `migrated-${event.toLowerCase()}-${handlerIndex}`,
135
+ id,
87
136
  type: 'script',
88
137
  command: hook.command,
89
138
  timeout: hook.timeout ? hook.timeout * 1000 : 5000, // Claude uses seconds, we use ms
@@ -91,6 +140,20 @@ function migrate(options) {
91
140
  };
92
141
  });
93
142
  }
143
+ // Install built-in hook scripts
144
+ (0, builtin_hooks_js_1.installBuiltinHooks)();
145
+ // Add update checker to SessionStart handlers
146
+ const checkUpdatePath = (0, path_1.join)(constants_js_1.HOOKS_DIR, 'check-update.js');
147
+ if (!manifestHandlers['SessionStart']) {
148
+ manifestHandlers['SessionStart'] = [];
149
+ }
150
+ manifestHandlers['SessionStart'].unshift({
151
+ id: 'clooks-check-update',
152
+ type: 'script',
153
+ command: `node ${checkUpdatePath}`,
154
+ timeout: 6000,
155
+ enabled: true,
156
+ });
94
157
  // Write manifest.yaml
95
158
  const manifest = {
96
159
  handlers: manifestHandlers,
@@ -0,0 +1,50 @@
1
+ import type { PluginManifest, PluginRegistry, InstalledPlugin, Manifest } from './types.js';
2
+ /**
3
+ * Load the plugin registry (installed.json).
4
+ */
5
+ export declare function loadRegistry(registryPath?: string): PluginRegistry;
6
+ /**
7
+ * Save the plugin registry.
8
+ */
9
+ export declare function saveRegistry(registry: PluginRegistry, registryPath?: string): void;
10
+ /**
11
+ * Validate a plugin manifest.
12
+ * Similar to validateManifest but checks plugin-specific fields (name, version required).
13
+ */
14
+ export declare function validatePluginManifest(manifest: PluginManifest): void;
15
+ /**
16
+ * Load all installed plugins and return their manifests.
17
+ */
18
+ export declare function loadPlugins(pluginsDir?: string, registryPath?: string): {
19
+ name: string;
20
+ manifest: PluginManifest;
21
+ }[];
22
+ /**
23
+ * Merge user manifest + plugin manifests into a composite manifest.
24
+ * Plugin handler IDs are namespaced as "pluginName/handlerId".
25
+ * Prefetch keys are unioned.
26
+ * Settings come from user manifest only.
27
+ */
28
+ export declare function mergeManifests(userManifest: Manifest, plugins: {
29
+ name: string;
30
+ manifest: PluginManifest;
31
+ }[]): Manifest;
32
+ /**
33
+ * Install a plugin from a local directory path.
34
+ * 1. Read clooks-plugin.yaml from the path
35
+ * 2. Validate it
36
+ * 3. Copy the directory to plugins dir under {name}/
37
+ * 4. Register in installed.json
38
+ * 5. Resolve $PLUGIN_DIR in handler commands to the installed path
39
+ */
40
+ export declare function installPlugin(sourcePath: string, pluginsDir?: string, registryPath?: string): InstalledPlugin;
41
+ /**
42
+ * Uninstall a plugin by name.
43
+ * 1. Remove from installed.json
44
+ * 2. Delete the plugin directory
45
+ */
46
+ export declare function uninstallPlugin(name: string, pluginsDir?: string, registryPath?: string): void;
47
+ /**
48
+ * List installed plugins.
49
+ */
50
+ export declare function listPlugins(registryPath?: string): InstalledPlugin[];