@goodfoot/agent-hooks 1.0.10 → 1.0.12
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/dist/agents/antigravity/scaffold.js +1 -1
- package/dist/agents/codex/scaffold.js +1 -1
- package/dist/cli.js +109 -47
- package/dist/core/transport.js +19 -8
- package/dist/scaffold.js +1 -1
- package/package.json +10 -5
- package/types/cli.d.ts +41 -15
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.12
|
|
4
|
+
|
|
5
|
+
Waits for stdout and stderr writes to complete before exiting, preventing truncated hook responses and error diagnostics when output is piped. Asynchronous stdout write failures now follow the existing unexpected-error policy without retrying the payload.
|
|
6
|
+
|
|
7
|
+
## 1.0.11
|
|
8
|
+
|
|
9
|
+
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.
|
|
7
10
|
|
|
8
11
|
## 1.0.10
|
|
9
12
|
|
|
@@ -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.12" },
|
|
54
54
|
devDependencies: {
|
|
55
55
|
"@biomejs/biome": "2.4.9",
|
|
56
56
|
"@types/node": "^24.0.0",
|
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.12";
|
|
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
|
};
|
|
@@ -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
|
}
|
|
@@ -1049,13 +1055,7 @@ function generateHooksJson(compiledHooks, buildDir, contextInfo, executable = "n
|
|
|
1049
1055
|
}
|
|
1050
1056
|
hooks[eventName] = entries;
|
|
1051
1057
|
}
|
|
1052
|
-
return {
|
|
1053
|
-
hooks,
|
|
1054
|
-
__generated: {
|
|
1055
|
-
files: compiledHooks.map((h) => h.outputFilename),
|
|
1056
|
-
timestamp: new Date().toISOString(),
|
|
1057
|
-
},
|
|
1058
|
-
};
|
|
1058
|
+
return { hooks };
|
|
1059
1059
|
}
|
|
1060
1060
|
/**
|
|
1061
1061
|
* Reads an existing hooks.json file if it exists.
|
|
@@ -1078,14 +1078,66 @@ function readExistingHooksJson(outputPath) {
|
|
|
1078
1078
|
}
|
|
1079
1079
|
}
|
|
1080
1080
|
/**
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* @
|
|
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
|
|
1084
1132
|
* @param outputDir - Directory containing the generated files
|
|
1133
|
+
* @param keepFilenames - Filenames written by the current build
|
|
1085
1134
|
*/
|
|
1086
|
-
function
|
|
1087
|
-
const filesToRemove =
|
|
1135
|
+
function removeStaleGeneratedFiles(previousMeta, outputDir, keepFilenames) {
|
|
1136
|
+
const filesToRemove = previousMeta?.files ?? [];
|
|
1088
1137
|
for (const filename of filesToRemove) {
|
|
1138
|
+
if (keepFilenames.has(filename)) {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1089
1141
|
const filePath = path.join(outputDir, filename);
|
|
1090
1142
|
if (fs.existsSync(filePath)) {
|
|
1091
1143
|
try {
|
|
@@ -1101,13 +1153,15 @@ function removeOldGeneratedFiles(existingHooksJson, outputDir) {
|
|
|
1101
1153
|
}
|
|
1102
1154
|
}
|
|
1103
1155
|
/**
|
|
1104
|
-
* Extracts hooks from an existing hooks.json that were NOT generated by this
|
|
1105
|
-
*
|
|
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.
|
|
1106
1159
|
* @param existingHooksJson - The existing hooks.json content
|
|
1160
|
+
* @param meta - Tracking sidecar from the previous build, if any
|
|
1107
1161
|
* @returns Object containing preserved hooks (keyed by event type)
|
|
1108
1162
|
*/
|
|
1109
|
-
function extractPreservedHooks(existingHooksJson) {
|
|
1110
|
-
const generatedFiles = new Set(
|
|
1163
|
+
function extractPreservedHooks(existingHooksJson, meta) {
|
|
1164
|
+
const generatedFiles = new Set(meta?.files ?? []);
|
|
1111
1165
|
const preserved = {};
|
|
1112
1166
|
for (const [eventType, entries] of Object.entries(existingHooksJson.hooks)) {
|
|
1113
1167
|
if (!entries)
|
|
@@ -1155,28 +1209,24 @@ function mergeHooksJson(newHooksJson, preservedHooks) {
|
|
|
1155
1209
|
// Combine preserved and generated entries
|
|
1156
1210
|
mergedHooks[eventType] = [...preserved, ...generated];
|
|
1157
1211
|
}
|
|
1158
|
-
return {
|
|
1159
|
-
hooks: mergedHooks,
|
|
1160
|
-
__generated: newHooksJson.__generated,
|
|
1161
|
-
};
|
|
1212
|
+
return { hooks: mergedHooks };
|
|
1162
1213
|
}
|
|
1163
1214
|
/**
|
|
1164
|
-
* Writes
|
|
1165
|
-
*
|
|
1166
|
-
* @param
|
|
1167
|
-
* @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
|
|
1168
1218
|
*/
|
|
1169
|
-
function
|
|
1170
|
-
const dir = path.dirname(
|
|
1219
|
+
function writeJsonAtomic(value, filePath) {
|
|
1220
|
+
const dir = path.dirname(filePath);
|
|
1171
1221
|
if (!fs.existsSync(dir)) {
|
|
1172
1222
|
fs.mkdirSync(dir, { recursive: true });
|
|
1173
1223
|
}
|
|
1174
1224
|
// Write to a temporary file first, then rename for atomicity
|
|
1175
|
-
const tempPath = `${
|
|
1176
|
-
const content = `${JSON.stringify(
|
|
1225
|
+
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
1226
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
1177
1227
|
try {
|
|
1178
1228
|
fs.writeFileSync(tempPath, content, "utf-8");
|
|
1179
|
-
fs.renameSync(tempPath,
|
|
1229
|
+
fs.renameSync(tempPath, filePath);
|
|
1180
1230
|
}
|
|
1181
1231
|
catch (error) {
|
|
1182
1232
|
// Clean up temp file if rename failed
|
|
@@ -1191,6 +1241,22 @@ function writeHooksJson(hooksJson, outputPath) {
|
|
|
1191
1241
|
throw error;
|
|
1192
1242
|
}
|
|
1193
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
|
+
}
|
|
1194
1260
|
// ============================================================================
|
|
1195
1261
|
// Main Entry Point
|
|
1196
1262
|
// ============================================================================
|
|
@@ -1376,15 +1442,14 @@ async function main() {
|
|
|
1376
1442
|
process.stderr.write(`Error: No hook files found matching pattern: ${args.input}\n`);
|
|
1377
1443
|
process.exit(1);
|
|
1378
1444
|
}
|
|
1379
|
-
// Read existing hooks.json to preserve non-generated hooks
|
|
1445
|
+
// Read existing hooks.json and its tracking sidecar to preserve non-generated hooks
|
|
1380
1446
|
const existingHooksJson = readExistingHooksJson(outputPath);
|
|
1447
|
+
const existingMeta = readHooksMeta(outputPath);
|
|
1381
1448
|
let preservedHooks = {};
|
|
1382
1449
|
if (existingHooksJson !== undefined) {
|
|
1383
1450
|
log("info", "Found existing hooks.json, will preserve non-generated hooks");
|
|
1384
1451
|
// Extract hooks that were NOT generated by this package
|
|
1385
|
-
preservedHooks = extractPreservedHooks(existingHooksJson);
|
|
1386
|
-
// Remove old generated files from disk
|
|
1387
|
-
removeOldGeneratedFiles(existingHooksJson, buildDir);
|
|
1452
|
+
preservedHooks = extractPreservedHooks(existingHooksJson, existingMeta);
|
|
1388
1453
|
const preservedCount = Object.values(preservedHooks).reduce((sum, entries) => sum + (entries?.reduce((s, e) => s + e.hooks.length, 0) ?? 0), 0);
|
|
1389
1454
|
log("info", `Preserved ${preservedCount} hooks from other sources`);
|
|
1390
1455
|
}
|
|
@@ -1408,19 +1473,16 @@ async function main() {
|
|
|
1408
1473
|
// Generate hooks.json for newly compiled hooks
|
|
1409
1474
|
const executable = args.executable !== undefined && args.executable !== "" ? args.executable : "node";
|
|
1410
1475
|
const newHooksJson = generateHooksJson(compiledHooks, buildDir, hookContext, executable);
|
|
1411
|
-
// Preserve timestamp if generated files haven't changed
|
|
1412
|
-
if (existingHooksJson !== undefined) {
|
|
1413
|
-
const existingFiles = [...(existingHooksJson.__generated?.files ?? [])].sort();
|
|
1414
|
-
const newFiles = [...newHooksJson.__generated.files].sort();
|
|
1415
|
-
const filesUnchanged = existingFiles.length === newFiles.length && existingFiles.every((f, i) => f === newFiles[i]);
|
|
1416
|
-
if (filesUnchanged && existingHooksJson.__generated?.timestamp) {
|
|
1417
|
-
newHooksJson.__generated.timestamp = existingHooksJson.__generated.timestamp;
|
|
1418
|
-
log("info", "Files unchanged, preserving existing timestamp");
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
1476
|
// Merge with preserved hooks
|
|
1422
1477
|
const finalHooksJson = mergeHooksJson(newHooksJson, preservedHooks);
|
|
1423
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);
|
|
1424
1486
|
log("info", "Compilation complete", {
|
|
1425
1487
|
hooksCompiled: compiledHooks.length,
|
|
1426
1488
|
outputPath,
|
|
@@ -1471,4 +1533,4 @@ if (isDirectExecution) {
|
|
|
1471
1533
|
// Shared build primitives reused by the per-agent CLI branches (single
|
|
1472
1534
|
// source for driver logic — the duplication check forbids re-deriving these
|
|
1473
1535
|
// inside agents/*).
|
|
1474
|
-
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, moduleWorkingDir, 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/core/transport.js
CHANGED
|
@@ -111,18 +111,29 @@ function classify(error, phase, policy, onUnexpectedError) {
|
|
|
111
111
|
}
|
|
112
112
|
return { kind: "handlerError", error, phase };
|
|
113
113
|
}
|
|
114
|
+
// A successful write return value only describes buffering; its callback confirms delivery.
|
|
115
|
+
function writeStream(stream, content) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
stream.write(content, (error) => {
|
|
118
|
+
if (error)
|
|
119
|
+
reject(error);
|
|
120
|
+
else
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
114
125
|
/**
|
|
115
126
|
* Writes an unexpected (non-handler, runtime-owned) error's stacktrace to
|
|
116
127
|
* stderr. Used for cleanup/finalize failures under the default `"error"`
|
|
117
128
|
* policy, which exit with code 1 — these are not the handler's own
|
|
118
129
|
* intentional signal.
|
|
119
130
|
*/
|
|
120
|
-
function writeUnexpectedErrorStderr(error) {
|
|
131
|
+
async function writeUnexpectedErrorStderr(error) {
|
|
121
132
|
if (error instanceof Error) {
|
|
122
|
-
process.stderr
|
|
133
|
+
await writeStream(process.stderr, `${error.stack ?? error.message}\n`);
|
|
123
134
|
}
|
|
124
135
|
else {
|
|
125
|
-
process.stderr
|
|
136
|
+
await writeStream(process.stderr, `${String(error)}\n`);
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
/**
|
|
@@ -198,7 +209,7 @@ export async function drive(transport, hookFn) {
|
|
|
198
209
|
cleanupQuietly();
|
|
199
210
|
process.exit(FALLBACK_EXIT_SUCCESS);
|
|
200
211
|
}
|
|
201
|
-
writeUnexpectedErrorStderr(error);
|
|
212
|
+
await writeUnexpectedErrorStderr(error);
|
|
202
213
|
cleanupQuietly();
|
|
203
214
|
process.exit(FALLBACK_EXIT_ERROR);
|
|
204
215
|
}
|
|
@@ -206,15 +217,15 @@ export async function drive(transport, hookFn) {
|
|
|
206
217
|
cleanup(policy, onUnexpectedError);
|
|
207
218
|
}
|
|
208
219
|
catch (error) {
|
|
209
|
-
writeUnexpectedErrorStderr(error);
|
|
220
|
+
await writeUnexpectedErrorStderr(error);
|
|
210
221
|
process.exit(FALLBACK_EXIT_ERROR);
|
|
211
222
|
}
|
|
212
223
|
if (finalized.stderr !== undefined) {
|
|
213
|
-
process.stderr
|
|
224
|
+
await writeStream(process.stderr, finalized.stderr);
|
|
214
225
|
}
|
|
215
226
|
if (finalized.stdout !== undefined) {
|
|
216
227
|
try {
|
|
217
|
-
process.stdout
|
|
228
|
+
await writeStream(process.stdout, finalized.stdout);
|
|
218
229
|
}
|
|
219
230
|
catch (error) {
|
|
220
231
|
if (policy === "continue") {
|
|
@@ -222,7 +233,7 @@ export async function drive(transport, hookFn) {
|
|
|
222
233
|
cleanupQuietly();
|
|
223
234
|
process.exit(FALLBACK_EXIT_SUCCESS);
|
|
224
235
|
}
|
|
225
|
-
writeUnexpectedErrorStderr(error);
|
|
236
|
+
await writeUnexpectedErrorStderr(error);
|
|
226
237
|
cleanupQuietly();
|
|
227
238
|
process.exit(FALLBACK_EXIT_ERROR);
|
|
228
239
|
}
|
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.12",
|
|
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.12",
|
|
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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
24
|
"build": "tsc --build tsconfig.build.json",
|
|
25
|
-
"lint": "biome check --write --unsafe --max-diagnostics 500 ./src ./tests ./e2e ./scripts && node ./scripts/check-stale-imports.mjs",
|
|
25
|
+
"lint": "biome check --write --unsafe --max-diagnostics 500 ./src ./tests ./e2e ./scripts ./vitest.config.ts && node ./scripts/check-stale-imports.mjs",
|
|
26
26
|
"test": "node ./scripts/check-stale-imports.mjs && node ./scripts/check-agent-duplication.mjs && vitest run",
|
|
27
27
|
"release": "bash ../../scripts/release-package.sh agent-hooks",
|
|
28
28
|
"release:dry-run": "bash ../../scripts/release-package.sh agent-hooks --dry-run"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"esbuild": "^0.
|
|
59
|
+
"esbuild": "^0.28.2",
|
|
60
60
|
"glob": "^11.0.0",
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
@@ -70,10 +70,15 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@anthropic-ai/claude-agent-sdk": "^0.3.162",
|
|
73
|
+
"@anthropic-ai/sdk": "^0.133.0",
|
|
73
74
|
"@biomejs/biome": "2.4.16",
|
|
75
|
+
"@goodfoot/vitest-cache": "workspace:*",
|
|
76
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
74
77
|
"@opencode-ai/plugin": "1.18.23",
|
|
75
|
-
"@types/node": "^
|
|
78
|
+
"@types/node": "^26.6.5",
|
|
76
79
|
"tsx": "^4.20.6",
|
|
77
|
-
"
|
|
80
|
+
"vite": "8.3.4",
|
|
81
|
+
"vitest": "patch:vitest@npm%3A5.0.4#~/.yarn/patches/vitest-npm-5.0.4-2ab4bceca5.patch",
|
|
82
|
+
"zod": "^4.6.7"
|
|
78
83
|
}
|
|
79
84
|
}
|
package/types/cli.d.ts
CHANGED
|
@@ -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.
|
|
@@ -301,19 +306,40 @@ declare function generateHooksJson(compiledHooks: CompiledHook[], buildDir: stri
|
|
|
301
306
|
*/
|
|
302
307
|
declare function readExistingHooksJson(outputPath: string): HooksJson | undefined;
|
|
303
308
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* @
|
|
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
|
|
307
330
|
* @param outputDir - Directory containing the generated files
|
|
331
|
+
* @param keepFilenames - Filenames written by the current build
|
|
308
332
|
*/
|
|
309
|
-
declare function
|
|
333
|
+
declare function removeStaleGeneratedFiles(previousMeta: HooksMeta | undefined, outputDir: string, keepFilenames: Set<string>): void;
|
|
310
334
|
/**
|
|
311
|
-
* Extracts hooks from an existing hooks.json that were NOT generated by this
|
|
312
|
-
*
|
|
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.
|
|
313
338
|
* @param existingHooksJson - The existing hooks.json content
|
|
339
|
+
* @param meta - Tracking sidecar from the previous build, if any
|
|
314
340
|
* @returns Object containing preserved hooks (keyed by event type)
|
|
315
341
|
*/
|
|
316
|
-
declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Record<HookEventName, MatcherEntry[]>>;
|
|
342
|
+
declare function extractPreservedHooks(existingHooksJson: HooksJson, meta: HooksMeta | undefined): Partial<Record<HookEventName, MatcherEntry[]>>;
|
|
317
343
|
/**
|
|
318
344
|
* Merges preserved hooks with newly generated hooks.
|
|
319
345
|
* Preserved hooks are added first, then new hooks are appended.
|
|
@@ -323,4 +349,4 @@ declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Re
|
|
|
323
349
|
*/
|
|
324
350
|
declare function mergeHooksJson(newHooksJson: HooksJson, preservedHooks: Partial<Record<HookEventName, MatcherEntry[]>>): HooksJson;
|
|
325
351
|
export type { CliArgs, CompiledHook, HookConfig, HookLoaderMap, HookMetadata, HooksJson, MatcherEntry };
|
|
326
|
-
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, moduleWorkingDir, 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, };
|