@goodfoot/claude-code-hooks 1.0.17 → 1.0.19

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
@@ -16,7 +16,6 @@
16
16
  */
17
17
  import * as crypto from "node:crypto";
18
18
  import * as fs from "node:fs";
19
- import * as os from "node:os";
20
19
  import * as path from "node:path";
21
20
  import * as esbuild from "esbuild";
22
21
  import { glob } from "glob";
@@ -374,46 +373,53 @@ async function discoverHookFiles(pattern, cwd) {
374
373
  /**
375
374
  * Compiles a TypeScript hook file to a self-contained ESM executable.
376
375
  *
377
- * Creates a wrapper that imports the hook and calls execute(), then bundles
378
- * everything together including the runtime.
376
+ * Uses esbuild's stdin option to avoid writing temporary wrapper files to disk.
377
+ * This produces stable, reproducible builds by:
378
+ * - Using a distinct sourcefile name to avoid import resolution conflicts
379
+ * - Eliminating environment-specific temp paths from source comments
380
+ *
381
+ * Uses a two-step process to generate stable content hashes:
382
+ * 1. Compile WITHOUT sourcemaps → generate stable content hash
383
+ * 2. Compile WITH sourcemaps → final output content
384
+ *
379
385
  * @param options - Compilation options
380
- * @returns Compiled output content as a string
386
+ * @returns Compiled content and stable content hash
381
387
  */
382
388
  async function compileHook(options) {
383
389
  const { sourcePath, logFilePath } = options;
384
- // Create a temporary wrapper file that imports the hook and executes it
385
- // Use system temp directory with deterministic name based on hook basename (not full path)
386
- // This ensures builds are reproducible across different environments/machines
387
- const hashInputs = [path.basename(sourcePath), logFilePath ? "log" : ""].join(":");
388
- const buildHash = crypto.createHash("sha256").update(hashInputs).digest("hex").substring(0, 16);
389
- const tempDir = path.join(os.tmpdir(), "claude-code-hooks-build", buildHash);
390
- const wrapperPath = path.join(tempDir, "wrapper.ts");
391
- const tempOutput = path.join(tempDir, "output.mjs");
392
390
  // Get the path to the runtime module (relative to this CLI)
393
391
  const runtimePath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "./runtime.js");
394
- // Ensure temp directory exists (don't delete - concurrent builds may be using it)
395
- fs.mkdirSync(tempDir, { recursive: true });
396
392
  // Build log file injection code if specified
397
393
  const logFileInjection = logFilePath !== undefined
398
394
  ? `process.env['CLAUDE_CODE_HOOKS_CLI_LOG_FILE'] = ${JSON.stringify(logFilePath)};\n`
399
395
  : "";
400
- // Create wrapper that imports the hook and calls execute
396
+ // Create wrapper content that imports the hook and calls execute
397
+ // Uses absolute paths to avoid resolution issues
401
398
  const wrapperContent = `${logFileInjection}
402
399
  import hook from '${sourcePath.replace(/\\/g, "/")}';
403
400
  import { execute } from '${runtimePath.replace(/\\/g, "/")}';
404
401
 
405
402
  execute(hook);
406
403
  `;
407
- fs.writeFileSync(wrapperPath, wrapperContent, "utf-8");
408
- await esbuild.build({
409
- entryPoints: [wrapperPath],
410
- outfile: tempOutput,
404
+ // Use stdin instead of a temp file - sourcefile becomes the stable reference
405
+ // The sourcefile name must be distinct from the actual source file to avoid
406
+ // esbuild resolving the import to the stdin content instead of the real file
407
+ const baseName = path.basename(sourcePath, path.extname(sourcePath));
408
+ const stdinOptions = {
409
+ contents: wrapperContent,
410
+ resolveDir: path.dirname(sourcePath),
411
+ sourcefile: `${baseName}-entry.ts`,
412
+ loader: "ts",
413
+ };
414
+ // Common esbuild options
415
+ const commonOptions = {
416
+ stdin: stdinOptions,
411
417
  format: "esm",
412
418
  platform: "node",
413
419
  target: "node20",
414
420
  bundle: true,
415
- sourcemap: "inline",
416
421
  minify: false,
422
+ write: false, // Return content directly via outputFiles
417
423
  // Keep node built-ins external
418
424
  external: [
419
425
  "node:*",
@@ -437,11 +443,27 @@ execute(hook);
437
443
  // Ensure we get clean ESM output
438
444
  mainFields: ["module", "main"],
439
445
  conditions: ["import", "node"],
446
+ };
447
+ // Step 1: Compile WITHOUT sourcemaps to generate stable content hash
448
+ const resultNoSourcemap = await esbuild.build({
449
+ ...commonOptions,
450
+ sourcemap: false,
451
+ });
452
+ const contentForHash = resultNoSourcemap.outputFiles?.[0]?.text;
453
+ if (contentForHash === undefined) {
454
+ throw new Error(`esbuild produced no output for ${sourcePath}`);
455
+ }
456
+ const contentHash = generateContentHash(contentForHash);
457
+ // Step 2: Compile WITH sourcemaps for final output
458
+ const resultWithSourcemap = await esbuild.build({
459
+ ...commonOptions,
460
+ sourcemap: "inline",
440
461
  });
441
- // Read and return the compiled content
442
- // Don't delete temp directory - allows concurrent builds of same source
443
- // and the OS will clean up /tmp periodically
444
- return fs.readFileSync(tempOutput, "utf-8");
462
+ const content = resultWithSourcemap.outputFiles?.[0]?.text;
463
+ if (content === undefined) {
464
+ throw new Error(`esbuild produced no output for ${sourcePath}`);
465
+ }
466
+ return { content, contentHash };
445
467
  }
446
468
  /**
447
469
  * Generates a content hash (SHA-256, 8-char prefix) for a compiled hook.
@@ -476,19 +498,17 @@ async function compileAllHooks(options) {
476
498
  matcher: metadata.matcher,
477
499
  timeout: metadata.timeout,
478
500
  });
479
- // Compile the hook
501
+ // Compile the hook (two-step process for stable content hash)
480
502
  log("info", `Compiling: ${sourcePath}`);
481
- const compiledContent = await compileHook({ sourcePath, outputDir, logFilePath });
482
- // Generate content hash
483
- const hash = generateContentHash(compiledContent);
484
- // Determine output filename
503
+ const { content, contentHash } = await compileHook({ sourcePath, outputDir, logFilePath });
504
+ // Determine output filename using the stable content hash
485
505
  const baseName = path.basename(sourcePath, path.extname(sourcePath));
486
- const outputFilename = `${baseName}.${hash}.mjs`;
506
+ const outputFilename = `${baseName}.${contentHash}.mjs`;
487
507
  const outputPath = path.join(outputDir, outputFilename);
488
508
  // Write compiled output with shebang for direct execution
489
509
  // --enable-source-maps enables stack traces with original source locations
490
510
  const shebang = "#!/usr/bin/env -S node --enable-source-maps\n";
491
- fs.writeFileSync(outputPath, shebang + compiledContent, { encoding: "utf-8", mode: 0o755 });
511
+ fs.writeFileSync(outputPath, shebang + content, { encoding: "utf-8", mode: 0o755 });
492
512
  log("info", `Wrote: ${outputPath}`);
493
513
  compiledHooks.push({
494
514
  sourcePath,
package/dist/scaffold.js CHANGED
@@ -117,7 +117,7 @@ function generatePackageJson(projectName, outputPath) {
117
117
  "@goodfoot/claude-code-hooks": "^1.0.9",
118
118
  },
119
119
  devDependencies: {
120
- "@biomejs/biome": "2.3.13",
120
+ "@biomejs/biome": "2.3.14",
121
121
  "@types/node": "^22.0.0",
122
122
  typescript: "^5.9.3",
123
123
  vitest: "^4.0.16",
@@ -160,7 +160,7 @@ function generateTsConfig() {
160
160
  */
161
161
  function generateBiomeConfig() {
162
162
  return `{
163
- "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
163
+ "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
164
164
  "formatter": {
165
165
  "enabled": true,
166
166
  "indentStyle": "space",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfoot/claude-code-hooks",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Type-safe Claude Code hooks library with camelCase types and output builders",
5
5
  "homepage": "https://github.com/goodfoot-io/marketplace/tree/main/packages/claude-code-hooks",
6
6
  "repository": {
@@ -53,8 +53,8 @@
53
53
  "typescript": "^5.9.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@anthropic-ai/claude-agent-sdk": "^0.2.22",
57
- "@biomejs/biome": "2.3.13",
56
+ "@anthropic-ai/claude-agent-sdk": "^0.2.31",
57
+ "@biomejs/biome": "2.3.14",
58
58
  "@types/node": "^24",
59
59
  "ts-morph": "^25.0.0",
60
60
  "tsx": "^4.20.3",
package/types/cli.d.ts CHANGED
@@ -151,15 +151,31 @@ interface CompileHookOptions {
151
151
  /** Optional log file path to inject into compiled hook. */
152
152
  logFilePath?: string;
153
153
  }
154
+ /**
155
+ * Result of compiling a hook.
156
+ */
157
+ interface CompileHookResult {
158
+ /** Compiled content WITH sourcemaps (for final output). */
159
+ content: string;
160
+ /** Stable content hash generated from sourcemap-free output. */
161
+ contentHash: string;
162
+ }
154
163
  /**
155
164
  * Compiles a TypeScript hook file to a self-contained ESM executable.
156
165
  *
157
- * Creates a wrapper that imports the hook and calls execute(), then bundles
158
- * everything together including the runtime.
166
+ * Uses esbuild's stdin option to avoid writing temporary wrapper files to disk.
167
+ * This produces stable, reproducible builds by:
168
+ * - Using a distinct sourcefile name to avoid import resolution conflicts
169
+ * - Eliminating environment-specific temp paths from source comments
170
+ *
171
+ * Uses a two-step process to generate stable content hashes:
172
+ * 1. Compile WITHOUT sourcemaps → generate stable content hash
173
+ * 2. Compile WITH sourcemaps → final output content
174
+ *
159
175
  * @param options - Compilation options
160
- * @returns Compiled output content as a string
176
+ * @returns Compiled content and stable content hash
161
177
  */
162
- declare function compileHook(options: CompileHookOptions): Promise<string>;
178
+ declare function compileHook(options: CompileHookOptions): Promise<CompileHookResult>;
163
179
  /**
164
180
  * Generates a content hash (SHA-256, 8-char prefix) for a compiled hook.
165
181
  * @param content - Compiled hook content
package/types/types.d.ts CHANGED
@@ -307,13 +307,7 @@ export type SubagentStartInput = SDKSubagentStartHookInput;
307
307
  * ```
308
308
  * @see https://code.claude.com/docs/en/hooks#subagentstop
309
309
  */
310
- export interface SubagentStopInput extends SDKSubagentStopHookInput {
311
- /**
312
- * Type of subagent that is stopping.
313
- * Examples: 'explore', 'codebase-analysis', custom agent types
314
- */
315
- agent_type: string;
316
- }
310
+ export type SubagentStopInput = SDKSubagentStopHookInput;
317
311
  /**
318
312
  * Input for PreCompact hooks.
319
313
  *