@mauribadnights/clooks 0.3.0 → 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.
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ const program = new commander_1.Command();
17
17
  program
18
18
  .name('clooks')
19
19
  .description('Persistent hook runtime for Claude Code')
20
- .version('0.3.0');
20
+ .version('0.3.1');
21
21
  // --- start ---
22
22
  program
23
23
  .command('start')
@@ -120,6 +120,9 @@ program
120
120
  .action(() => {
121
121
  const metrics = new metrics_js_1.MetricsCollector();
122
122
  console.log(metrics.formatStatsTable());
123
+ console.log('');
124
+ console.log('Per Handler:');
125
+ console.log(metrics.formatHandlerStatsTable());
123
126
  // Append cost summary if LLM data exists
124
127
  const costStats = metrics.getCostStats();
125
128
  if (costStats.totalCost > 0) {
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,6 +2,7 @@
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");
@@ -26,6 +27,46 @@ function getSettingsPath(options) {
26
27
  }
27
28
  return null;
28
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
+ }
29
70
  /**
30
71
  * Migrate Claude Code settings.json command hooks to clooks HTTP hooks.
31
72
  *
@@ -61,6 +102,7 @@ function migrate(options) {
61
102
  // Extract command hooks and build manifest
62
103
  const manifestHandlers = {};
63
104
  let handlerIndex = 0;
105
+ const usedIds = new Set();
64
106
  // NOTE: In v0.1, matchers from the original rule groups are not preserved in the
65
107
  // migrated HTTP hooks — all command hooks are consolidated into matcher-less rule groups.
66
108
  // This is acceptable because clooks dispatches based on event type, not matchers.
@@ -83,8 +125,14 @@ function migrate(options) {
83
125
  continue;
84
126
  manifestHandlers[event] = commandHooks.map((hook) => {
85
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);
86
134
  return {
87
- id: `migrated-${event.toLowerCase()}-${handlerIndex}`,
135
+ id,
88
136
  type: 'script',
89
137
  command: hook.command,
90
138
  timeout: hook.timeout ? hook.timeout * 1000 : 5000, // Claude uses seconds, we use ms
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauribadnights/clooks",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Persistent hook runtime for Claude Code — eliminates process spawning overhead and gives you observability",
5
5
  "bin": {
6
6
  "clooks": "./dist/cli.js"