@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 +4 -1
- package/dist/metrics.d.ts +14 -0
- package/dist/metrics.js +51 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.js +49 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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