@goodfoot/agent-hooks 1.0.9 → 1.0.11

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/CHANGELOG.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # @goodfoot/agent-hooks npm package changelog
2
2
 
3
- ## 1.0.9
4
- - Added support for Antigravity hooks, including event handling, output formatting, and transport
5
- - Added scaffolding for Antigravity hook projects via the CLI
6
- - Documented the Antigravity hook contract in the README
3
+ ## 1.0.11
4
+
5
+ Moves generated-file tracking out of `hooks.json` into a sibling `hooks.meta.json`, because Claude Code rejects a hooks manifest carrying any key besides `hooks`. The manifest is now a pure function of its inputs, so a rebuild that changes nothing writes identical bytes and leaves the host's hook-trust hash intact. A missing or unreadable sidecar is treated as "nothing tracked as generated", which preserves the entries already in the manifest rather than pruning them.
6
+
7
+ ## 1.0.10
8
+
9
+ Makes compiled bundles portable across ordinary hoisted workspace layouts by anchoring esbuild's module-boundary comments and sourcemap source paths at the directory that owns the resolved `node_modules`. Inline sourcemaps are now disabled by default for smaller, reviewable build artifacts and remain available with the explicit `--sourcemap` flag.
7
10
 
8
11
  ## 1.0.9
9
12
 
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Build AI coding agent hooks in TypeScript — one package, any supported agent.**
4
4
 
5
+ Compiled bundles omit inline sourcemaps by default. Pass `--sourcemap` when original TypeScript locations are needed in runtime stack traces.
6
+
5
7
  `@goodfoot/agent-hooks` is the unified successor to `@goodfoot/claude-code-hooks` and
6
8
  `@goodfoot/codex-hooks`: a shared agent-neutral core (hook factory primitive, transport
7
9
  driver, `Logger`, env/stdin utilities) with a per-agent entry point on top. Write
@@ -22,7 +22,7 @@ import * as path from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import * as esbuild from "esbuild";
24
24
  import ts from "typescript";
25
- import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
25
+ import { buildEsbuildLoaderMap, discoverHookSourceFiles, moduleWorkingDir, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
26
26
  import { DEFAULT_TIMEOUT_SECONDS, GROUPED_EVENTS, HOOK_FACTORY_TO_EVENT } from "./constants.js";
27
27
  import { HOOK_EVENT_NAMES } from "./events.js";
28
28
  /** The matcher emitted when a grouped event's config names none. `"*"` matches every tool. */
@@ -82,9 +82,10 @@ export function analyzeAntigravityHookFile(sourcePath) {
82
82
  * Bundles one hook source into a self-contained ESM executable whose entry
83
83
  * wrapper calls this agent's `execute`.
84
84
  */
85
- export async function compileAntigravityHook(sourcePath, loaders, sourcemap = true) {
85
+ export async function compileAntigravityHook(sourcePath, loaders, sourcemap = false) {
86
86
  const resolveDir = path.dirname(sourcePath);
87
87
  const runtimeAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./transport.js"), resolveDir);
88
+ const absWorkingDir = moduleWorkingDir(runtimeAbsolute, resolveDir);
88
89
  const runtimeRelative = path.relative(resolveDir, runtimeAbsolute).replace(/\\/g, "/");
89
90
  const runtimeSpecifier = runtimeRelative.startsWith(".") ? runtimeRelative : `./${runtimeRelative}`;
90
91
  const entry = [
@@ -94,6 +95,7 @@ export async function compileAntigravityHook(sourcePath, loaders, sourcemap = tr
94
95
  ``,
95
96
  ].join("\n");
96
97
  const built = await esbuild.build({
98
+ absWorkingDir,
97
99
  stdin: {
98
100
  contents: entry,
99
101
  resolveDir,
@@ -50,7 +50,7 @@ function packageJson(projectName, outputPath) {
50
50
  lint: "biome check .",
51
51
  typecheck: "tsc --noEmit",
52
52
  },
53
- dependencies: { [PACKAGE_NAME]: "^1.0.9" },
53
+ dependencies: { [PACKAGE_NAME]: "^1.0.11" },
54
54
  devDependencies: {
55
55
  "@biomejs/biome": "2.4.9",
56
56
  "@types/node": "^24.0.0",
@@ -15,7 +15,7 @@ import * as path from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import * as esbuild from "esbuild";
17
17
  import ts from "typescript";
18
- import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
18
+ import { buildEsbuildLoaderMap, discoverHookSourceFiles, moduleWorkingDir, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
19
19
  import { EVENTS_WITH_MATCHER, HOOK_FACTORY_TO_EVENT } from "./constants.js";
20
20
  /**
21
21
  * Extracts Codex hook metadata (event name, matcher, timeout in
@@ -88,9 +88,10 @@ export function analyzeCodexHookFile(sourcePath) {
88
88
  * Compiles one Codex hook to a self-contained ESM executable whose entry
89
89
  * wrapper imports this package's Codex runtime module (`execute`).
90
90
  */
91
- export async function compileCodexHook(sourcePath, loaders, sourcemap = true) {
91
+ export async function compileCodexHook(sourcePath, loaders, sourcemap = false) {
92
92
  const resolveDir = path.dirname(sourcePath);
93
93
  const runtimePathAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./transport.js"), resolveDir);
94
+ const absWorkingDir = moduleWorkingDir(runtimePathAbsolute, resolveDir);
94
95
  let relativeRuntimePath = path.relative(resolveDir, runtimePathAbsolute).replace(/\\/g, "/");
95
96
  if (!relativeRuntimePath.startsWith(".")) {
96
97
  relativeRuntimePath = `./${relativeRuntimePath}`;
@@ -102,6 +103,7 @@ execute(hook);
102
103
  `;
103
104
  const baseName = path.basename(sourcePath, path.extname(sourcePath));
104
105
  const result = await esbuild.build({
106
+ absWorkingDir,
105
107
  stdin: {
106
108
  contents: wrapperContent,
107
109
  resolveDir,
@@ -55,7 +55,7 @@ function generatePackageJson(projectName, outputPath) {
55
55
  typecheck: "tsc --noEmit",
56
56
  },
57
57
  dependencies: {
58
- [PACKAGE_NAME]: "^1.0.9",
58
+ [PACKAGE_NAME]: "^1.0.11",
59
59
  },
60
60
  devDependencies: {
61
61
  "@biomejs/biome": "2.4.9",
@@ -23,7 +23,7 @@ import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag
23
23
  * Bundles one OpenCode plugin entry to a self-contained ESM artifact. The
24
24
  * source file's own default export is preserved unchanged — no wrapper.
25
25
  */
26
- export async function compileOpenCodePlugin(sourcePath, loaders, sourcemap = true) {
26
+ export async function compileOpenCodePlugin(sourcePath, loaders, sourcemap = false) {
27
27
  const result = await esbuild.build({
28
28
  entryPoints: [sourcePath],
29
29
  loader: loaders,
package/dist/cli.js CHANGED
@@ -26,7 +26,9 @@ import { scaffoldProject } from "./scaffold.js";
26
26
  // ============================================================================
27
27
  // Constants
28
28
  // ============================================================================
29
- const VERSION = "1.0.9";
29
+ const VERSION = "1.0.11";
30
+ /** Filename of the tracking sidecar written beside hooks.json. */
31
+ const HOOKS_META_FILENAME = "hooks.meta.json";
30
32
  const DEFAULT_ESBUILD_LOADERS = {
31
33
  ".md": "text",
32
34
  };
@@ -142,13 +144,13 @@ Optional Arguments:
142
144
  --no-stable-names
143
145
  Restore the pre-1.7 behavior: hashed compiled bundles (<name>.<hash>.mjs).
144
146
 
145
- --sourcemap (default)
147
+ --sourcemap
146
148
  Embed an inline sourcemap in each compiled bundle so stack traces show
147
149
  original TypeScript source locations.
148
150
 
149
- --no-sourcemap
150
- Emit bundles without the inline sourcemap, shrinking compiled output by
151
- roughly 85-90%. The content hash is unaffected.
151
+ --no-sourcemap (default)
152
+ Emit bundles without the inline sourcemap, shrinking compiled output.
153
+ The content hash is unaffected.
152
154
 
153
155
  -h, --help
154
156
  Show this help message.
@@ -257,7 +259,7 @@ function parseArgs(argv) {
257
259
  // as true, while the Codex branch defaults by command context
258
260
  // (plugin mode → stable, otherwise hashed).
259
261
  stableNames: undefined,
260
- sourcemap: true,
262
+ sourcemap: false,
261
263
  };
262
264
  for (let i = 0; i < argv.length; i++) {
263
265
  const arg = argv[i];
@@ -584,8 +586,12 @@ function symlinkVisiblePath(realPath, resolveDir) {
584
586
  pkgRel = path.relative(dir, realPath);
585
587
  }
586
588
  }
587
- catch {
588
- // Not a readable package.json — the realpath is not under a package.
589
+ catch (error) {
590
+ // An unreadable or malformed manifest is the same case as no manifest
591
+ // at all: identity stays undefined and the caller falls back below.
592
+ log("debug", `Could not read package manifest: ${pkgJsonPath}`, {
593
+ error: error instanceof Error ? error.message : String(error),
594
+ });
589
595
  }
590
596
  break;
591
597
  }
@@ -642,6 +648,28 @@ function symlinkVisiblePath(realPath, resolveDir) {
642
648
  dir = parent;
643
649
  }
644
650
  }
651
+ /**
652
+ * Returns the stable project anchor for a module reached through node_modules.
653
+ *
654
+ * esbuild otherwise uses process.cwd() when rendering module-boundary comments
655
+ * and sourcemap source paths. A workspace script normally runs from the
656
+ * consuming package, while its dependencies may be hoisted several directories
657
+ * higher, making those emitted paths depend on the consumer's nesting depth.
658
+ * Anchoring at the directory which owns the visible node_modules keeps the
659
+ * canonical `node_modules/...` form in every such layout.
660
+ */
661
+ function moduleWorkingDir(modulePath, fallback) {
662
+ for (let dir = path.dirname(modulePath);;) {
663
+ if (path.basename(dir) === "node_modules") {
664
+ return path.dirname(dir);
665
+ }
666
+ const parent = path.dirname(dir);
667
+ if (parent === dir) {
668
+ return fallback;
669
+ }
670
+ dir = parent;
671
+ }
672
+ }
645
673
  /**
646
674
  * Compiles a TypeScript hook file to a self-contained ESM executable.
647
675
  *
@@ -672,6 +700,7 @@ async function compileHook(options) {
672
700
  // nesting depth.
673
701
  const resolveDir = path.dirname(sourcePath);
674
702
  const runtimePathAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./agents/claude-code/transport.js"), resolveDir);
703
+ const absWorkingDir = moduleWorkingDir(runtimePathAbsolute, resolveDir);
675
704
  // Compute relative paths from resolveDir to avoid absolute paths in source maps.
676
705
  // This ensures reproducible builds regardless of checkout directory.
677
706
  const relativeSourcePath = `./${path.basename(sourcePath)}`;
@@ -731,6 +760,7 @@ execute(hook);
731
760
  ].join("\n");
732
761
  // Common esbuild options
733
762
  const commonOptions = {
763
+ absWorkingDir,
734
764
  stdin: stdinOptions,
735
765
  loader: loaders,
736
766
  format: "esm",
@@ -784,7 +814,7 @@ execute(hook);
784
814
  // output minus the trailing sourceMappingURL comment (verified on esbuild
785
815
  // 0.24.2), so the sourcemap-free content is emitted directly. The content
786
816
  // hash is always derived from pass-1 output, so it is unchanged by the flag.
787
- if (sourcemap === false) {
817
+ if (sourcemap !== true) {
788
818
  return { content: contentForHash, contentHash };
789
819
  }
790
820
  const resultWithSourcemap = await esbuild.build({
@@ -1025,13 +1055,7 @@ function generateHooksJson(compiledHooks, buildDir, contextInfo, executable = "n
1025
1055
  }
1026
1056
  hooks[eventName] = entries;
1027
1057
  }
1028
- return {
1029
- hooks,
1030
- __generated: {
1031
- files: compiledHooks.map((h) => h.outputFilename),
1032
- timestamp: new Date().toISOString(),
1033
- },
1034
- };
1058
+ return { hooks };
1035
1059
  }
1036
1060
  /**
1037
1061
  * Reads an existing hooks.json file if it exists.
@@ -1054,14 +1078,66 @@ function readExistingHooksJson(outputPath) {
1054
1078
  }
1055
1079
  }
1056
1080
  /**
1057
- * Removes previously generated hook files from disk.
1058
- * Only removes files that were tracked in __generated.files.
1059
- * @param existingHooksJson - The existing hooks.json content
1081
+ * Absolute path of the tracking sidecar that sits beside hooks.json.
1082
+ * @param outputPath - Path to hooks.json
1083
+ * @returns Path to hooks.meta.json
1084
+ */
1085
+ function hooksMetaPath(outputPath) {
1086
+ return path.join(path.dirname(outputPath), HOOKS_META_FILENAME);
1087
+ }
1088
+ /**
1089
+ * Reads the tracking sidecar beside hooks.json if it exists.
1090
+ *
1091
+ * A missing or unreadable sidecar is not fatal: it only means no generated
1092
+ * filenames are known, so every existing entry is treated as externally
1093
+ * authored and preserved.
1094
+ * @param outputPath - Path to the hooks.json the sidecar describes
1095
+ * @returns Parsed HooksMeta or undefined if absent or unusable
1096
+ */
1097
+ function readHooksMeta(outputPath) {
1098
+ const metaPath = hooksMetaPath(outputPath);
1099
+ if (!fs.existsSync(metaPath)) {
1100
+ return undefined;
1101
+ }
1102
+ try {
1103
+ const parsed = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1104
+ const rawFiles = parsed.files;
1105
+ if (!Array.isArray(rawFiles)) {
1106
+ log("warn", "hooks.meta.json has no files array, treating generated hooks as untracked", { path: metaPath });
1107
+ return undefined;
1108
+ }
1109
+ const files = rawFiles.filter((entry) => typeof entry === "string");
1110
+ if (files.length !== rawFiles.length) {
1111
+ log("warn", "hooks.meta.json lists non-string filenames, treating generated hooks as untracked", {
1112
+ path: metaPath,
1113
+ });
1114
+ return undefined;
1115
+ }
1116
+ return { files };
1117
+ }
1118
+ catch (error) {
1119
+ log("warn", "Failed to parse hooks.meta.json, treating generated hooks as untracked", {
1120
+ path: metaPath,
1121
+ error: error instanceof Error ? error.message : String(error),
1122
+ });
1123
+ return undefined;
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Removes bundles a previous build generated that this build did not. Only
1128
+ * filenames tracked in the sidecar are ever removed, and callers must run this
1129
+ * after the new hooks.json is on disk so no reachable state loses a file it
1130
+ * references.
1131
+ * @param previousMeta - Tracking sidecar from the previous build, if any
1060
1132
  * @param outputDir - Directory containing the generated files
1133
+ * @param keepFilenames - Filenames written by the current build
1061
1134
  */
1062
- function removeOldGeneratedFiles(existingHooksJson, outputDir) {
1063
- const filesToRemove = existingHooksJson.__generated?.files ?? [];
1135
+ function removeStaleGeneratedFiles(previousMeta, outputDir, keepFilenames) {
1136
+ const filesToRemove = previousMeta?.files ?? [];
1064
1137
  for (const filename of filesToRemove) {
1138
+ if (keepFilenames.has(filename)) {
1139
+ continue;
1140
+ }
1065
1141
  const filePath = path.join(outputDir, filename);
1066
1142
  if (fs.existsSync(filePath)) {
1067
1143
  try {
@@ -1077,13 +1153,15 @@ function removeOldGeneratedFiles(existingHooksJson, outputDir) {
1077
1153
  }
1078
1154
  }
1079
1155
  /**
1080
- * Extracts hooks from an existing hooks.json that were NOT generated by this package.
1081
- * Identifies generated hooks by checking if their command path matches the generated file pattern.
1156
+ * Extracts hooks from an existing hooks.json that were NOT generated by this
1157
+ * package. Generated hooks are identified by the filenames tracked in the
1158
+ * sidecar, so entries authored anywhere else survive a rebuild untouched.
1082
1159
  * @param existingHooksJson - The existing hooks.json content
1160
+ * @param meta - Tracking sidecar from the previous build, if any
1083
1161
  * @returns Object containing preserved hooks (keyed by event type)
1084
1162
  */
1085
- function extractPreservedHooks(existingHooksJson) {
1086
- const generatedFiles = new Set(existingHooksJson.__generated?.files ?? []);
1163
+ function extractPreservedHooks(existingHooksJson, meta) {
1164
+ const generatedFiles = new Set(meta?.files ?? []);
1087
1165
  const preserved = {};
1088
1166
  for (const [eventType, entries] of Object.entries(existingHooksJson.hooks)) {
1089
1167
  if (!entries)
@@ -1131,28 +1209,24 @@ function mergeHooksJson(newHooksJson, preservedHooks) {
1131
1209
  // Combine preserved and generated entries
1132
1210
  mergedHooks[eventType] = [...preserved, ...generated];
1133
1211
  }
1134
- return {
1135
- hooks: mergedHooks,
1136
- __generated: newHooksJson.__generated,
1137
- };
1212
+ return { hooks: mergedHooks };
1138
1213
  }
1139
1214
  /**
1140
- * Writes hooks.json to the specified path atomically.
1141
- * Uses write-to-temp-then-rename pattern for atomicity.
1142
- * @param hooksJson - The hooks.json content
1143
- * @param outputPath - Path to write hooks.json
1215
+ * Writes a JSON document atomically, using write-to-temp-then-rename.
1216
+ * @param value - Value to serialize
1217
+ * @param filePath - Destination path
1144
1218
  */
1145
- function writeHooksJson(hooksJson, outputPath) {
1146
- const dir = path.dirname(outputPath);
1219
+ function writeJsonAtomic(value, filePath) {
1220
+ const dir = path.dirname(filePath);
1147
1221
  if (!fs.existsSync(dir)) {
1148
1222
  fs.mkdirSync(dir, { recursive: true });
1149
1223
  }
1150
1224
  // Write to a temporary file first, then rename for atomicity
1151
- const tempPath = `${outputPath}.tmp.${process.pid}`;
1152
- const content = `${JSON.stringify(hooksJson, null, 2)}\n`;
1225
+ const tempPath = `${filePath}.tmp.${process.pid}`;
1226
+ const content = `${JSON.stringify(value, null, 2)}\n`;
1153
1227
  try {
1154
1228
  fs.writeFileSync(tempPath, content, "utf-8");
1155
- fs.renameSync(tempPath, outputPath);
1229
+ fs.renameSync(tempPath, filePath);
1156
1230
  }
1157
1231
  catch (error) {
1158
1232
  // Clean up temp file if rename failed
@@ -1167,6 +1241,22 @@ function writeHooksJson(hooksJson, outputPath) {
1167
1241
  throw error;
1168
1242
  }
1169
1243
  }
1244
+ /**
1245
+ * Writes hooks.json to the specified path atomically.
1246
+ * @param hooksJson - The hooks.json content
1247
+ * @param outputPath - Path to write hooks.json
1248
+ */
1249
+ function writeHooksJson(hooksJson, outputPath) {
1250
+ writeJsonAtomic(hooksJson, outputPath);
1251
+ }
1252
+ /**
1253
+ * Writes the tracking sidecar beside hooks.json atomically.
1254
+ * @param meta - Tracking metadata for this build
1255
+ * @param outputPath - Path to the hooks.json the sidecar describes
1256
+ */
1257
+ function writeHooksMeta(meta, outputPath) {
1258
+ writeJsonAtomic(meta, hooksMetaPath(outputPath));
1259
+ }
1170
1260
  // ============================================================================
1171
1261
  // Main Entry Point
1172
1262
  // ============================================================================
@@ -1352,15 +1442,14 @@ async function main() {
1352
1442
  process.stderr.write(`Error: No hook files found matching pattern: ${args.input}\n`);
1353
1443
  process.exit(1);
1354
1444
  }
1355
- // Read existing hooks.json to preserve non-generated hooks
1445
+ // Read existing hooks.json and its tracking sidecar to preserve non-generated hooks
1356
1446
  const existingHooksJson = readExistingHooksJson(outputPath);
1447
+ const existingMeta = readHooksMeta(outputPath);
1357
1448
  let preservedHooks = {};
1358
1449
  if (existingHooksJson !== undefined) {
1359
1450
  log("info", "Found existing hooks.json, will preserve non-generated hooks");
1360
1451
  // Extract hooks that were NOT generated by this package
1361
- preservedHooks = extractPreservedHooks(existingHooksJson);
1362
- // Remove old generated files from disk
1363
- removeOldGeneratedFiles(existingHooksJson, buildDir);
1452
+ preservedHooks = extractPreservedHooks(existingHooksJson, existingMeta);
1364
1453
  const preservedCount = Object.values(preservedHooks).reduce((sum, entries) => sum + (entries?.reduce((s, e) => s + e.hooks.length, 0) ?? 0), 0);
1365
1454
  log("info", `Preserved ${preservedCount} hooks from other sources`);
1366
1455
  }
@@ -1384,19 +1473,16 @@ async function main() {
1384
1473
  // Generate hooks.json for newly compiled hooks
1385
1474
  const executable = args.executable !== undefined && args.executable !== "" ? args.executable : "node";
1386
1475
  const newHooksJson = generateHooksJson(compiledHooks, buildDir, hookContext, executable);
1387
- // Preserve timestamp if generated files haven't changed
1388
- if (existingHooksJson !== undefined) {
1389
- const existingFiles = [...(existingHooksJson.__generated?.files ?? [])].sort();
1390
- const newFiles = [...newHooksJson.__generated.files].sort();
1391
- const filesUnchanged = existingFiles.length === newFiles.length && existingFiles.every((f, i) => f === newFiles[i]);
1392
- if (filesUnchanged && existingHooksJson.__generated?.timestamp) {
1393
- newHooksJson.__generated.timestamp = existingHooksJson.__generated.timestamp;
1394
- log("info", "Files unchanged, preserving existing timestamp");
1395
- }
1396
- }
1397
1476
  // Merge with preserved hooks
1398
1477
  const finalHooksJson = mergeHooksJson(newHooksJson, preservedHooks);
1399
1478
  writeHooksJson(finalHooksJson, outputPath);
1479
+ // Drop the bundles the previous build produced that this one did not, then
1480
+ // record what this build wrote. Stale files go only after the hooks.json
1481
+ // that references the current set is on disk, so every state on disk
1482
+ // between the two writes stays runnable.
1483
+ const writtenFilenames = new Set(compiledHooks.map((hook) => hook.outputFilename));
1484
+ removeStaleGeneratedFiles(existingMeta, buildDir, writtenFilenames);
1485
+ writeHooksMeta({ files: [...writtenFilenames].sort() }, outputPath);
1400
1486
  log("info", "Compilation complete", {
1401
1487
  hooksCompiled: compiledHooks.length,
1402
1488
  outputPath,
@@ -1447,4 +1533,4 @@ if (isDirectExecution) {
1447
1533
  // Shared build primitives reused by the per-agent CLI branches (single
1448
1534
  // source for driver logic — the duplication check forbids re-deriving these
1449
1535
  // inside agents/*).
1450
- export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, mergeHooksJson, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
1536
+ export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, hooksMetaPath, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, readHooksMeta, removeStaleGeneratedFiles, symlinkVisiblePath, validateArgs, };
package/dist/scaffold.js CHANGED
@@ -131,7 +131,7 @@ function generatePackageJson(projectName, outputPath) {
131
131
  typecheck: "tsc --noEmit",
132
132
  },
133
133
  dependencies: {
134
- "@goodfoot/agent-hooks": "^1.0.9",
134
+ "@goodfoot/agent-hooks": "^1.0.11",
135
135
  },
136
136
  devDependencies: {
137
137
  "@biomejs/biome": "2.4.16",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfoot/agent-hooks",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Unified agent hooks library: shared core runtime with per-agent entry points for Claude Code, Codex, and Antigravity",
5
5
  "homepage": "https://github.com/goodfoot-io/marketplace/tree/main/packages/agent-hooks",
6
6
  "repository": {
package/types/cli.d.ts CHANGED
@@ -54,7 +54,7 @@ interface CliArgs {
54
54
  pluginRoot?: boolean;
55
55
  /** Emit hash-free `<name>.mjs` bundles. Defaults to true; set false with --no-stable-names. */
56
56
  stableNames?: boolean;
57
- /** Embed an inline sourcemap in compiled bundles. Defaults to true; set false with --no-sourcemap. */
57
+ /** Embed an inline sourcemap in compiled bundles. Defaults to false; enable with --sourcemap. */
58
58
  sourcemap?: boolean;
59
59
  }
60
60
  type HookLoaderMap = Record<string, esbuild.Loader>;
@@ -106,17 +106,22 @@ interface MatcherEntry {
106
106
  * The complete hooks.json structure expected by Claude Code.
107
107
  *
108
108
  * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
109
+ *
110
+ * `hooks` is the only key that may appear here: the host validates hooks.json
111
+ * against its own schema and rejects files carrying unknown keys, so build
112
+ * tracking lives in the sidecar instead (see HooksMeta).
109
113
  */
110
114
  interface HooksJson {
111
115
  /** Object keyed by event type (PreToolUse, SessionStart, etc.). */
112
116
  hooks: Partial<Record<HookEventName, MatcherEntry[]>>;
113
- /** Generated file tracking metadata. */
114
- __generated: {
115
- /** Array of generated filenames. */
116
- files: string[];
117
- /** ISO timestamp of generation. */
118
- timestamp: string;
119
- };
117
+ }
118
+ /**
119
+ * Build tracking metadata for a generated hooks.json, written to a sidecar
120
+ * file beside it (hooks.meta.json) rather than inside hooks.json itself.
121
+ */
122
+ interface HooksMeta {
123
+ /** Bundle filenames this package generated, relative to the bin/ directory. */
124
+ files: string[];
120
125
  }
121
126
  /**
122
127
  * Parses command-line arguments.
@@ -173,14 +178,14 @@ interface CompileHookOptions {
173
178
  logEnvVar?: string;
174
179
  /** Explicit esbuild loaders for non-code imports. */
175
180
  loaders: HookLoaderMap;
176
- /** Embed an inline sourcemap in the compiled content. Defaults to true; set false with --no-sourcemap. */
181
+ /** Embed an inline sourcemap in the compiled content. Defaults to false; enable with --sourcemap. */
177
182
  sourcemap?: boolean;
178
183
  }
179
184
  /**
180
185
  * Result of compiling a hook.
181
186
  */
182
187
  interface CompileHookResult {
183
- /** Compiled content, with inline sourcemaps unless disabled. */
188
+ /** Compiled content, with inline sourcemaps when enabled. */
184
189
  content: string;
185
190
  /** Stable content hash generated from sourcemap-free output. */
186
191
  contentHash: string;
@@ -203,6 +208,17 @@ interface CompileHookResult {
203
208
  * checkout-local.
204
209
  */
205
210
  declare function symlinkVisiblePath(realPath: string, resolveDir: string): string;
211
+ /**
212
+ * Returns the stable project anchor for a module reached through node_modules.
213
+ *
214
+ * esbuild otherwise uses process.cwd() when rendering module-boundary comments
215
+ * and sourcemap source paths. A workspace script normally runs from the
216
+ * consuming package, while its dependencies may be hoisted several directories
217
+ * higher, making those emitted paths depend on the consumer's nesting depth.
218
+ * Anchoring at the directory which owns the visible node_modules keeps the
219
+ * canonical `node_modules/...` form in every such layout.
220
+ */
221
+ declare function moduleWorkingDir(modulePath: string, fallback: string): string;
206
222
  /**
207
223
  * Compiles a TypeScript hook file to a self-contained ESM executable.
208
224
  *
@@ -290,19 +306,40 @@ declare function generateHooksJson(compiledHooks: CompiledHook[], buildDir: stri
290
306
  */
291
307
  declare function readExistingHooksJson(outputPath: string): HooksJson | undefined;
292
308
  /**
293
- * Removes previously generated hook files from disk.
294
- * Only removes files that were tracked in __generated.files.
295
- * @param existingHooksJson - The existing hooks.json content
309
+ * Absolute path of the tracking sidecar that sits beside hooks.json.
310
+ * @param outputPath - Path to hooks.json
311
+ * @returns Path to hooks.meta.json
312
+ */
313
+ declare function hooksMetaPath(outputPath: string): string;
314
+ /**
315
+ * Reads the tracking sidecar beside hooks.json if it exists.
316
+ *
317
+ * A missing or unreadable sidecar is not fatal: it only means no generated
318
+ * filenames are known, so every existing entry is treated as externally
319
+ * authored and preserved.
320
+ * @param outputPath - Path to the hooks.json the sidecar describes
321
+ * @returns Parsed HooksMeta or undefined if absent or unusable
322
+ */
323
+ declare function readHooksMeta(outputPath: string): HooksMeta | undefined;
324
+ /**
325
+ * Removes bundles a previous build generated that this build did not. Only
326
+ * filenames tracked in the sidecar are ever removed, and callers must run this
327
+ * after the new hooks.json is on disk so no reachable state loses a file it
328
+ * references.
329
+ * @param previousMeta - Tracking sidecar from the previous build, if any
296
330
  * @param outputDir - Directory containing the generated files
331
+ * @param keepFilenames - Filenames written by the current build
297
332
  */
298
- declare function removeOldGeneratedFiles(existingHooksJson: HooksJson, outputDir: string): void;
333
+ declare function removeStaleGeneratedFiles(previousMeta: HooksMeta | undefined, outputDir: string, keepFilenames: Set<string>): void;
299
334
  /**
300
- * Extracts hooks from an existing hooks.json that were NOT generated by this package.
301
- * Identifies generated hooks by checking if their command path matches the generated file pattern.
335
+ * Extracts hooks from an existing hooks.json that were NOT generated by this
336
+ * package. Generated hooks are identified by the filenames tracked in the
337
+ * sidecar, so entries authored anywhere else survive a rebuild untouched.
302
338
  * @param existingHooksJson - The existing hooks.json content
339
+ * @param meta - Tracking sidecar from the previous build, if any
303
340
  * @returns Object containing preserved hooks (keyed by event type)
304
341
  */
305
- declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Record<HookEventName, MatcherEntry[]>>;
342
+ declare function extractPreservedHooks(existingHooksJson: HooksJson, meta: HooksMeta | undefined): Partial<Record<HookEventName, MatcherEntry[]>>;
306
343
  /**
307
344
  * Merges preserved hooks with newly generated hooks.
308
345
  * Preserved hooks are added first, then new hooks are appended.
@@ -312,4 +349,4 @@ declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Re
312
349
  */
313
350
  declare function mergeHooksJson(newHooksJson: HooksJson, preservedHooks: Partial<Record<HookEventName, MatcherEntry[]>>): HooksJson;
314
351
  export type { CliArgs, CompiledHook, HookConfig, HookLoaderMap, HookMetadata, HooksJson, MatcherEntry };
315
- export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, mergeHooksJson, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
352
+ export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, hooksMetaPath, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, readHooksMeta, removeStaleGeneratedFiles, symlinkVisiblePath, validateArgs, };