@goodfoot/claude-code-hooks 1.0.10 → 1.0.15

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/logger.js CHANGED
@@ -19,12 +19,12 @@
19
19
  * ```
20
20
  * @see https://code.claude.com/docs/en/hooks
21
21
  */
22
- import { closeSync, existsSync, mkdirSync, openSync, writeSync } from 'node:fs';
23
- import { dirname } from 'node:path';
22
+ import { closeSync, existsSync, mkdirSync, openSync, writeSync } from "node:fs";
23
+ import { dirname } from "node:path";
24
24
  /**
25
25
  * All log levels in order of severity (lowest to highest).
26
26
  */
27
- export const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
27
+ export const LOG_LEVELS = ["debug", "info", "warn", "error"];
28
28
  // ============================================================================
29
29
  // Logger Class
30
30
  // ============================================================================
@@ -63,376 +63,385 @@ export const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
63
63
  * ```
64
64
  */
65
65
  export class Logger {
66
- /**
67
- * Registered event handlers by log level.
68
- */
69
- handlers = new Map();
70
- /**
71
- * File descriptor for log file output.
72
- * Lazily initialized on first write.
73
- */
74
- logFileFd = null;
75
- /**
76
- * Path to the log file, if configured.
77
- */
78
- logFilePath = null;
79
- /**
80
- * Whether file initialization has been attempted.
81
- */
82
- fileInitialized = false;
83
- /**
84
- * Current hook context for enriching log events.
85
- */
86
- currentHookType;
87
- /**
88
- * Current hook input for enriching log events.
89
- */
90
- currentInput;
91
- /**
92
- * Creates a new Logger instance.
93
- *
94
- * Typically you should use the exported `logger` singleton rather than
95
- * creating new instances.
96
- * @param config - Optional configuration
97
- * @example
98
- * ```typescript
99
- * // Use singleton (recommended)
100
- * import { logger } from '@goodfoot/claude-code-hooks';
101
- *
102
- * // Or create custom instance
103
- * const customLogger = new Logger({ logFilePath: '/var/log/hooks.log' });
104
- * ```
105
- */
106
- constructor(config = {}) {
107
- // Initialize handlers map for each level
108
- for (const level of LOG_LEVELS) {
109
- this.handlers.set(level, new Set());
66
+ /**
67
+ * Registered event handlers by log level.
68
+ */
69
+ handlers = new Map();
70
+ /**
71
+ * File descriptor for log file output.
72
+ * Lazily initialized on first write.
73
+ */
74
+ logFileFd = null;
75
+ /**
76
+ * Path to the log file, if configured.
77
+ */
78
+ logFilePath = null;
79
+ /**
80
+ * Whether file initialization has been attempted.
81
+ */
82
+ fileInitialized = false;
83
+ /**
84
+ * Current hook context for enriching log events.
85
+ */
86
+ currentHookType;
87
+ /**
88
+ * Current hook input for enriching log events.
89
+ */
90
+ currentInput;
91
+ /**
92
+ * Creates a new Logger instance.
93
+ *
94
+ * Typically you should use the exported `logger` singleton rather than
95
+ * creating new instances.
96
+ * @param config - Optional configuration
97
+ * @example
98
+ * ```typescript
99
+ * // Use singleton (recommended)
100
+ * import { logger } from '@goodfoot/claude-code-hooks';
101
+ *
102
+ * // Or create custom instance
103
+ * const customLogger = new Logger({ logFilePath: '/var/log/hooks.log' });
104
+ * ```
105
+ */
106
+ constructor(config = {}) {
107
+ // Initialize handlers map for each level
108
+ for (const level of LOG_LEVELS) {
109
+ this.handlers.set(level, new Set());
110
+ }
111
+ // Set log file path from config or environment
112
+ this.logFilePath = config.logFilePath ?? process.env.CLAUDE_CODE_HOOKS_LOG_FILE ?? null;
110
113
  }
111
- // Set log file path from config or environment
112
- this.logFilePath = config.logFilePath ?? process.env['CLAUDE_CODE_HOOKS_LOG_FILE'] ?? null;
113
- }
114
- /**
115
- * Logs a debug message.
116
- *
117
- * Use for detailed debugging information that is typically only useful
118
- * during development or troubleshooting.
119
- * @param message - The debug message
120
- * @param context - Optional additional context
121
- * @example
122
- * ```typescript
123
- * logger.debug('Processing tool input', { toolName: 'Bash', inputSize: 256 });
124
- * ```
125
- */
126
- debug(message, context) {
127
- this.emit('debug', message, context);
128
- }
129
- /**
130
- * Logs an info message.
131
- *
132
- * Use for general operational events like hook invocations, successful
133
- * completions, or state changes.
134
- * @param message - The info message
135
- * @param context - Optional additional context
136
- * @example
137
- * ```typescript
138
- * logger.info('Session started', { source: 'startup', sessionId: 'abc123' });
139
- * ```
140
- */
141
- info(message, context) {
142
- this.emit('info', message, context);
143
- }
144
- /**
145
- * Logs a warning message.
146
- *
147
- * Use for conditions that may indicate issues but don't prevent
148
- * operation, such as deprecated patterns or performance concerns.
149
- * @param message - The warning message
150
- * @param context - Optional additional context
151
- * @example
152
- * ```typescript
153
- * logger.warn('Deprecated hook pattern detected', { pattern: 'legacyMatcher' });
154
- * ```
155
- */
156
- warn(message, context) {
157
- this.emit('warn', message, context);
158
- }
159
- /**
160
- * Logs an error message.
161
- *
162
- * Use for error conditions that require attention but were handled
163
- * gracefully. For exceptions, prefer {@link logError}.
164
- * @param message - The error message
165
- * @param context - Optional additional context
166
- * @example
167
- * ```typescript
168
- * logger.error('Failed to validate tool input', { toolName: 'Bash', reason: 'empty command' });
169
- * ```
170
- */
171
- error(message, context) {
172
- this.emit('error', message, context);
173
- }
174
- /**
175
- * Logs a structured error with full error details.
176
- *
177
- * Use this method when logging caught exceptions to capture the full
178
- * error context including name, message, stack trace, and cause chain.
179
- * @param error - The error to log
180
- * @param message - Human-readable description of what failed
181
- * @param context - Optional additional context
182
- * @example
183
- * ```typescript
184
- * try {
185
- * await dangerousOperation();
186
- * } catch (err) {
187
- * logger.logError(err, 'Failed to execute dangerous operation', {
188
- * operation: 'delete',
189
- * target: '/important/file.txt'
190
- * });
191
- * }
192
- * ```
193
- */
194
- logError(error, message, context) {
195
- const errorInfo = this.extractErrorInfo(error);
196
- const event = {
197
- timestamp: new Date().toISOString(),
198
- level: 'error',
199
- hookType: this.currentHookType,
200
- message,
201
- input: this.currentInput,
202
- error: errorInfo,
203
- context
204
- };
205
- this.deliverEvent(event);
206
- }
207
- /**
208
- * Subscribes a handler to log events at the specified level.
209
- *
210
- * The handler will be called for every log event at the specified level.
211
- * Returns an unsubscribe function that should be called when the handler
212
- * is no longer needed.
213
- * @param level - The log level to subscribe to
214
- * @param handler - The handler function to call for each event
215
- * @returns A function to unsubscribe the handler
216
- * @example
217
- * ```typescript
218
- * // Subscribe to error events
219
- * const unsubscribe = logger.on('error', (event) => {
220
- * console.error(`[${event.hookType}] ${event.message}`);
221
- * if (event.error) {
222
- * console.error(event.error.stack);
223
- * }
224
- * });
225
- *
226
- * // Later, clean up
227
- * unsubscribe();
228
- * ```
229
- * @example
230
- * ```typescript
231
- * // Forward to external logging library
232
- * import pino from 'pino';
233
- * const pinoLogger = pino();
234
- *
235
- * logger.on('info', (event) => pinoLogger.info(event, event.message));
236
- * logger.on('warn', (event) => pinoLogger.warn(event, event.message));
237
- * logger.on('error', (event) => pinoLogger.error(event, event.message));
238
- * ```
239
- */
240
- on(level, handler) {
241
- const levelHandlers = this.handlers.get(level);
242
- if (levelHandlers) {
243
- levelHandlers.add(handler);
114
+ /**
115
+ * Logs a debug message.
116
+ *
117
+ * Use for detailed debugging information that is typically only useful
118
+ * during development or troubleshooting.
119
+ * @param message - The debug message
120
+ * @param context - Optional additional context
121
+ * @example
122
+ * ```typescript
123
+ * logger.debug('Processing tool input', { toolName: 'Bash', inputSize: 256 });
124
+ * ```
125
+ */
126
+ debug(message, context) {
127
+ this.emit("debug", message, context);
244
128
  }
245
- return () => {
246
- levelHandlers?.delete(handler);
247
- };
248
- }
249
- /**
250
- * Sets the current hook context for enriching log events.
251
- *
252
- * This is called internally by the runtime before invoking hook handlers.
253
- * You typically don't need to call this directly.
254
- * @param hookType - The type of hook being executed
255
- * @param input - The hook input data
256
- * @internal
257
- */
258
- setContext(hookType, input) {
259
- this.currentHookType = hookType;
260
- this.currentInput = input;
261
- }
262
- /**
263
- * Clears the current hook context.
264
- *
265
- * Called internally by the runtime after hook execution completes.
266
- * @internal
267
- */
268
- clearContext() {
269
- this.currentHookType = undefined;
270
- this.currentInput = undefined;
271
- }
272
- /**
273
- * Configures the log file path at runtime.
274
- *
275
- * Call this to enable or change file logging. Setting to `null` disables
276
- * file logging (but doesn't close existing file handle immediately).
277
- * @param filePath - Path to the log file, or null to disable
278
- * @example
279
- * ```typescript
280
- * // Enable file logging at runtime
281
- * logger.setLogFile('/var/log/claude-hooks.log');
282
- *
283
- * // Disable file logging
284
- * logger.setLogFile(null);
285
- * ```
286
- */
287
- setLogFile(filePath) {
288
- // Close existing file if open
289
- if (this.logFileFd !== null) {
290
- try {
291
- closeSync(this.logFileFd);
292
- } catch {
293
- // Ignore errors on close
294
- }
295
- this.logFileFd = null;
129
+ /**
130
+ * Logs an info message.
131
+ *
132
+ * Use for general operational events like hook invocations, successful
133
+ * completions, or state changes.
134
+ * @param message - The info message
135
+ * @param context - Optional additional context
136
+ * @example
137
+ * ```typescript
138
+ * logger.info('Session started', { source: 'startup', sessionId: 'abc123' });
139
+ * ```
140
+ */
141
+ info(message, context) {
142
+ this.emit("info", message, context);
296
143
  }
297
- this.logFilePath = filePath;
298
- this.fileInitialized = false;
299
- }
300
- /**
301
- * Closes all resources held by the logger.
302
- *
303
- * Call this during graceful shutdown to ensure all log data is flushed.
304
- * @example
305
- * ```typescript
306
- * process.on('exit', () => {
307
- * logger.close();
308
- * });
309
- * ```
310
- */
311
- close() {
312
- if (this.logFileFd !== null) {
313
- try {
314
- closeSync(this.logFileFd);
315
- } catch {
316
- // Ignore errors on close
317
- }
318
- this.logFileFd = null;
144
+ /**
145
+ * Logs a warning message.
146
+ *
147
+ * Use for conditions that may indicate issues but don't prevent
148
+ * operation, such as deprecated patterns or performance concerns.
149
+ * @param message - The warning message
150
+ * @param context - Optional additional context
151
+ * @example
152
+ * ```typescript
153
+ * logger.warn('Deprecated hook pattern detected', { pattern: 'legacyMatcher' });
154
+ * ```
155
+ */
156
+ warn(message, context) {
157
+ this.emit("warn", message, context);
319
158
  }
320
- this.fileInitialized = false;
321
- }
322
- /**
323
- * Checks if there are any active handlers or destinations.
324
- *
325
- * Returns true if any handlers are registered or file logging is enabled.
326
- * @returns Whether the logger has any active output destinations
327
- */
328
- hasDestinations() {
329
- for (const handlers of this.handlers.values()) {
330
- if (handlers.size > 0) return true;
159
+ /**
160
+ * Logs an error message.
161
+ *
162
+ * Use for error conditions that require attention but were handled
163
+ * gracefully. For exceptions, prefer {@link logError}.
164
+ * @param message - The error message
165
+ * @param context - Optional additional context
166
+ * @example
167
+ * ```typescript
168
+ * logger.error('Failed to validate tool input', { toolName: 'Bash', reason: 'empty command' });
169
+ * ```
170
+ */
171
+ error(message, context) {
172
+ this.emit("error", message, context);
331
173
  }
332
- return this.logFilePath !== null;
333
- }
334
- // ============================================================================
335
- // Private Methods
336
- // ============================================================================
337
- /**
338
- * Emits a log event.
339
- * @param level - The severity level of the event
340
- * @param message - The log message
341
- * @param context - Optional additional context data
342
- */
343
- emit(level, message, context) {
344
- const event = {
345
- timestamp: new Date().toISOString(),
346
- level,
347
- hookType: this.currentHookType,
348
- message,
349
- input: this.currentInput,
350
- context
351
- };
352
- this.deliverEvent(event);
353
- }
354
- /**
355
- * Delivers an event to all registered destinations.
356
- * @param event - The log event to deliver
357
- */
358
- deliverEvent(event) {
359
- // Deliver to event handlers
360
- const levelHandlers = this.handlers.get(event.level);
361
- if (levelHandlers) {
362
- for (const handler of levelHandlers) {
363
- try {
364
- handler(event);
365
- } catch {
366
- // Silently ignore handler errors to not disrupt hook execution
174
+ /**
175
+ * Logs a structured error with full error details.
176
+ *
177
+ * Use this method when logging caught exceptions to capture the full
178
+ * error context including name, message, stack trace, and cause chain.
179
+ * @param error - The error to log
180
+ * @param message - Human-readable description of what failed
181
+ * @param context - Optional additional context
182
+ * @example
183
+ * ```typescript
184
+ * try {
185
+ * await dangerousOperation();
186
+ * } catch (err) {
187
+ * logger.logError(err, 'Failed to execute dangerous operation', {
188
+ * operation: 'delete',
189
+ * target: '/important/file.txt'
190
+ * });
191
+ * }
192
+ * ```
193
+ */
194
+ logError(error, message, context) {
195
+ const errorInfo = this.extractErrorInfo(error);
196
+ const event = {
197
+ timestamp: new Date().toISOString(),
198
+ level: "error",
199
+ hookType: this.currentHookType,
200
+ message,
201
+ input: this.currentInput,
202
+ error: errorInfo,
203
+ context,
204
+ };
205
+ this.deliverEvent(event);
206
+ }
207
+ /**
208
+ * Subscribes a handler to log events at the specified level.
209
+ *
210
+ * The handler will be called for every log event at the specified level.
211
+ * Returns an unsubscribe function that should be called when the handler
212
+ * is no longer needed.
213
+ * @param level - The log level to subscribe to
214
+ * @param handler - The handler function to call for each event
215
+ * @returns A function to unsubscribe the handler
216
+ * @example
217
+ * ```typescript
218
+ * // Subscribe to error events
219
+ * const unsubscribe = logger.on('error', (event) => {
220
+ * console.error(`[${event.hookType}] ${event.message}`);
221
+ * if (event.error) {
222
+ * console.error(event.error.stack);
223
+ * }
224
+ * });
225
+ *
226
+ * // Later, clean up
227
+ * unsubscribe();
228
+ * ```
229
+ * @example
230
+ * ```typescript
231
+ * // Forward to external logging library
232
+ * import pino from 'pino';
233
+ * const pinoLogger = pino();
234
+ *
235
+ * logger.on('info', (event) => pinoLogger.info(event, event.message));
236
+ * logger.on('warn', (event) => pinoLogger.warn(event, event.message));
237
+ * logger.on('error', (event) => pinoLogger.error(event, event.message));
238
+ * ```
239
+ */
240
+ on(level, handler) {
241
+ const levelHandlers = this.handlers.get(level);
242
+ if (levelHandlers) {
243
+ levelHandlers.add(handler);
367
244
  }
368
- }
245
+ return () => {
246
+ levelHandlers?.delete(handler);
247
+ };
369
248
  }
370
- // Write to file if configured
371
- this.writeToFile(event);
372
- }
373
- /**
374
- * Writes an event to the log file.
375
- * @param event - The log event to write
376
- */
377
- writeToFile(event) {
378
- if (!this.logFilePath) return;
379
- // Lazy initialization of file handle
380
- if (!this.fileInitialized) {
381
- this.initializeFile();
249
+ /**
250
+ * Sets the current hook context for enriching log events.
251
+ *
252
+ * This is called internally by the runtime before invoking hook handlers.
253
+ * You typically don't need to call this directly.
254
+ * @param hookType - The type of hook being executed
255
+ * @param input - The hook input data
256
+ * @internal
257
+ */
258
+ setContext(hookType, input) {
259
+ this.currentHookType = hookType;
260
+ this.currentInput = input;
382
261
  }
383
- if (this.logFileFd === null) return;
384
- try {
385
- const line = JSON.stringify(event) + '\n';
386
- writeSync(this.logFileFd, line);
387
- } catch {
388
- // Silently ignore file write errors to not disrupt hook execution
389
- // This follows the risk mitigation: "Graceful degradation - log write
390
- // failures are silently ignored to not disrupt hook execution"
262
+ /**
263
+ * Clears the current hook context.
264
+ *
265
+ * Called internally by the runtime after hook execution completes.
266
+ * @internal
267
+ */
268
+ clearContext() {
269
+ this.currentHookType = undefined;
270
+ this.currentInput = undefined;
391
271
  }
392
- }
393
- /**
394
- * Initializes the log file for writing.
395
- */
396
- initializeFile() {
397
- this.fileInitialized = true;
398
- if (!this.logFilePath) return;
399
- try {
400
- // Ensure directory exists
401
- const dir = dirname(this.logFilePath);
402
- if (!existsSync(dir)) {
403
- mkdirSync(dir, { recursive: true });
404
- }
405
- // Open file for appending
406
- this.logFileFd = openSync(this.logFilePath, 'a');
407
- } catch {
408
- // Silently ignore file initialization errors
409
- this.logFileFd = null;
272
+ /**
273
+ * Configures the log file path at runtime.
274
+ *
275
+ * Call this to enable or change file logging. Setting to `null` disables
276
+ * file logging (but doesn't close existing file handle immediately).
277
+ * @param filePath - Path to the log file, or null to disable
278
+ * @example
279
+ * ```typescript
280
+ * // Enable file logging at runtime
281
+ * logger.setLogFile('/var/log/claude-hooks.log');
282
+ *
283
+ * // Disable file logging
284
+ * logger.setLogFile(null);
285
+ * ```
286
+ */
287
+ setLogFile(filePath) {
288
+ // Close existing file if open
289
+ if (this.logFileFd !== null) {
290
+ try {
291
+ closeSync(this.logFileFd);
292
+ }
293
+ catch {
294
+ // Ignore errors on close
295
+ }
296
+ this.logFileFd = null;
297
+ }
298
+ this.logFilePath = filePath;
299
+ this.fileInitialized = false;
300
+ }
301
+ /**
302
+ * Closes all resources held by the logger.
303
+ *
304
+ * Call this during graceful shutdown to ensure all log data is flushed.
305
+ * @example
306
+ * ```typescript
307
+ * process.on('exit', () => {
308
+ * logger.close();
309
+ * });
310
+ * ```
311
+ */
312
+ close() {
313
+ if (this.logFileFd !== null) {
314
+ try {
315
+ closeSync(this.logFileFd);
316
+ }
317
+ catch {
318
+ // Ignore errors on close
319
+ }
320
+ this.logFileFd = null;
321
+ }
322
+ this.fileInitialized = false;
323
+ }
324
+ /**
325
+ * Checks if there are any active handlers or destinations.
326
+ *
327
+ * Returns true if any handlers are registered or file logging is enabled.
328
+ * @returns Whether the logger has any active output destinations
329
+ */
330
+ hasDestinations() {
331
+ for (const handlers of this.handlers.values()) {
332
+ if (handlers.size > 0)
333
+ return true;
334
+ }
335
+ return this.logFilePath !== null;
336
+ }
337
+ // ============================================================================
338
+ // Private Methods
339
+ // ============================================================================
340
+ /**
341
+ * Emits a log event.
342
+ * @param level - The severity level of the event
343
+ * @param message - The log message
344
+ * @param context - Optional additional context data
345
+ */
346
+ emit(level, message, context) {
347
+ const event = {
348
+ timestamp: new Date().toISOString(),
349
+ level,
350
+ hookType: this.currentHookType,
351
+ message,
352
+ input: this.currentInput,
353
+ context,
354
+ };
355
+ this.deliverEvent(event);
356
+ }
357
+ /**
358
+ * Delivers an event to all registered destinations.
359
+ * @param event - The log event to deliver
360
+ */
361
+ deliverEvent(event) {
362
+ // Deliver to event handlers
363
+ const levelHandlers = this.handlers.get(event.level);
364
+ if (levelHandlers) {
365
+ for (const handler of levelHandlers) {
366
+ try {
367
+ handler(event);
368
+ }
369
+ catch {
370
+ // Silently ignore handler errors to not disrupt hook execution
371
+ }
372
+ }
373
+ }
374
+ // Write to file if configured
375
+ this.writeToFile(event);
410
376
  }
411
- }
412
- /**
413
- * Extracts structured error information from an unknown error.
414
- * @param error - The error to extract information from
415
- * @returns Structured error information
416
- */
417
- extractErrorInfo(error) {
418
- if (error instanceof Error) {
419
- const info = {
420
- name: error.name,
421
- message: error.message,
422
- stack: error.stack
423
- };
424
- // Extract cause chain if present
425
- if (error.cause !== undefined) {
426
- info.cause = this.extractErrorInfo(error.cause);
427
- }
428
- return info;
377
+ /**
378
+ * Writes an event to the log file.
379
+ * @param event - The log event to write
380
+ */
381
+ writeToFile(event) {
382
+ if (!this.logFilePath)
383
+ return;
384
+ // Lazy initialization of file handle
385
+ if (!this.fileInitialized) {
386
+ this.initializeFile();
387
+ }
388
+ if (this.logFileFd === null)
389
+ return;
390
+ try {
391
+ const line = `${JSON.stringify(event)}\n`;
392
+ writeSync(this.logFileFd, line);
393
+ }
394
+ catch {
395
+ // Silently ignore file write errors to not disrupt hook execution
396
+ // This follows the risk mitigation: "Graceful degradation - log write
397
+ // failures are silently ignored to not disrupt hook execution"
398
+ }
399
+ }
400
+ /**
401
+ * Initializes the log file for writing.
402
+ */
403
+ initializeFile() {
404
+ this.fileInitialized = true;
405
+ if (!this.logFilePath)
406
+ return;
407
+ try {
408
+ // Ensure directory exists
409
+ const dir = dirname(this.logFilePath);
410
+ if (!existsSync(dir)) {
411
+ mkdirSync(dir, { recursive: true });
412
+ }
413
+ // Open file for appending
414
+ this.logFileFd = openSync(this.logFilePath, "a");
415
+ }
416
+ catch {
417
+ // Silently ignore file initialization errors
418
+ this.logFileFd = null;
419
+ }
420
+ }
421
+ /**
422
+ * Extracts structured error information from an unknown error.
423
+ * @param error - The error to extract information from
424
+ * @returns Structured error information
425
+ */
426
+ extractErrorInfo(error) {
427
+ if (error instanceof Error) {
428
+ const info = {
429
+ name: error.name,
430
+ message: error.message,
431
+ stack: error.stack,
432
+ };
433
+ // Extract cause chain if present
434
+ if (error.cause !== undefined) {
435
+ info.cause = this.extractErrorInfo(error.cause);
436
+ }
437
+ return info;
438
+ }
439
+ // Handle non-Error values
440
+ return {
441
+ name: "UnknownError",
442
+ message: String(error),
443
+ };
429
444
  }
430
- // Handle non-Error values
431
- return {
432
- name: 'UnknownError',
433
- message: String(error)
434
- };
435
- }
436
445
  }
437
446
  // ============================================================================
438
447
  // Singleton Export