@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 +7 -4
- package/README.md +2 -0
- package/dist/agents/antigravity/cli-support.js +4 -2
- package/dist/agents/antigravity/scaffold.js +1 -1
- package/dist/agents/codex/cli-support.js +4 -2
- package/dist/agents/codex/scaffold.js +1 -1
- package/dist/agents/opencode/cli-support.js +1 -1
- package/dist/cli.js +139 -53
- package/dist/scaffold.js +1 -1
- package/package.json +1 -1
- package/types/cli.d.ts +55 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @goodfoot/agent-hooks npm package changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
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 =
|
|
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.
|
|
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 =
|
|
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,
|
|
@@ -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 =
|
|
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.
|
|
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
|
|
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
|
|
151
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
* @
|
|
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
|
|
1063
|
-
const filesToRemove =
|
|
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
|
|
1081
|
-
*
|
|
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(
|
|
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
|
|
1141
|
-
*
|
|
1142
|
-
* @param
|
|
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
|
|
1146
|
-
const dir = path.dirname(
|
|
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 = `${
|
|
1152
|
-
const content = `${JSON.stringify(
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* @
|
|
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
|
|
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
|
|
301
|
-
*
|
|
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,
|
|
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, };
|