@goodfoot/agent-hooks 1.0.7 → 1.0.9

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.
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Antigravity output builders, one per event.
3
+ *
4
+ * Unlike Claude Code and Codex, Antigravity gives each event a **different**
5
+ * stdout shape — there is no single envelope every event shares. `PreToolUse`
6
+ * returns a permission decision, `PostToolUse` returns `{}` and nothing else,
7
+ * the two invocation events return injected steps, and `Stop` returns a
8
+ * continue-or-not decision. Each builder below emits only the fields its own
9
+ * event's contract names; see `CONTRACT.md` in this directory.
10
+ * @module
11
+ */
1
12
  import { HookBlockError } from "../../core/transport.js";
2
13
  /**
3
14
  * Antigravity has no exit-code signaling channel: every event replies at
@@ -15,14 +26,16 @@ export const EXIT_CODES = {
15
26
  * **before** consulting `unexpectedError` policy — exactly mirroring Claude
16
27
  * Code's `HookBlockError` usage and Codex's `BlockError` re-export.
17
28
  *
18
- * This mechanism is a stated *prerequisite*, not a follow-up, for
19
- * Antigravity specifically: because every Antigravity event replies at exit
20
- * 0 with the decision expressed only in the JSON payload (Research, plan
21
- * Step 5), a handler that throws mid-decision has no narrower exit-code
22
- * fallback to land on the way Codex's stderr+exit-2 channel does — the block
23
- * decision would otherwise be indistinguishable from a swallowed crash. This
24
- * class landing is what makes it safe to ever widen `events.ts`'s advisory
25
- * allow-list past empty.
29
+ * The class matters more here than on the other two agents: because every
30
+ * Antigravity event replies at exit 0 with the decision expressed only in the
31
+ * JSON payload, a handler that throws mid-decision has no narrower exit-code
32
+ * fallback to land on the way Codex's stderr+exit-2 channel does. Without
33
+ * this class the block would be indistinguishable from a swallowed crash.
34
+ *
35
+ * The transport serializes it as `{ "decision": "deny", ... }`, which the
36
+ * host acts on for `PreToolUse` only. On the other four events the reply is
37
+ * well-formed but carries no decision the host recognizes, so it reads as an
38
+ * empty response.
26
39
  */
27
40
  export class AntigravityBlockError extends HookBlockError {
28
41
  reason;
@@ -41,41 +54,35 @@ function buildOutput(type, stdout) {
41
54
  stdout: omitUndefined(stdout),
42
55
  };
43
56
  }
57
+ /** Builds a `PreToolUse` reply: a permission decision, optionally with argument overwrites. */
44
58
  export function preToolUseOutput(options = {}) {
45
59
  return buildOutput("PreToolUse", {
46
60
  decision: options.decision,
47
61
  reason: options.reason,
48
- additionalContext: options.additionalContext,
49
- updatedInput: options.updatedInput,
50
- systemMessage: options.systemMessage,
62
+ permissionOverrides: options.permissionOverrides,
63
+ overwrite: options.overwrite,
51
64
  });
52
65
  }
53
- export function postToolUseOutput(options = {}) {
54
- return buildOutput("PostToolUse", {
55
- decision: options.decision,
56
- reason: options.reason,
57
- additionalContext: options.additionalContext,
58
- systemMessage: options.systemMessage,
59
- });
66
+ /**
67
+ * Builds the `PostToolUse` reply. The contract defines the reply as an empty
68
+ * object, so this builder takes no options: there is nothing a `PostToolUse`
69
+ * handler can tell the host.
70
+ */
71
+ export function postToolUseOutput() {
72
+ return buildOutput("PostToolUse", {});
60
73
  }
74
+ /** Builds a `PreInvocation` reply carrying steps to inject before the model runs. */
61
75
  export function preInvocationOutput(options = {}) {
62
- return buildOutput("PreInvocation", {
63
- decision: options.decision,
64
- reason: options.reason,
65
- additionalContext: options.additionalContext,
66
- systemMessage: options.systemMessage,
67
- });
76
+ return buildOutput("PreInvocation", { injectSteps: options.injectSteps });
68
77
  }
78
+ /** Builds a `PostInvocation` reply: injected steps, a termination override, or both. */
69
79
  export function postInvocationOutput(options = {}) {
70
80
  return buildOutput("PostInvocation", {
71
- additionalContext: options.additionalContext,
72
- systemMessage: options.systemMessage,
81
+ injectSteps: options.injectSteps,
82
+ terminationBehavior: options.terminationBehavior,
73
83
  });
74
84
  }
85
+ /** Builds a `Stop` reply. `decision: "continue"` blocks the stop; `reason` is injected as a system message. */
75
86
  export function stopOutput(options = {}) {
76
- return buildOutput("Stop", {
77
- stop: options.stop,
78
- reason: options.reason,
79
- systemMessage: options.systemMessage,
80
- });
87
+ return buildOutput("Stop", { decision: options.decision, reason: options.reason });
81
88
  }
@@ -0,0 +1,211 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { HOOK_FACTORY_TO_EVENT, PACKAGE_NAME } from "./constants.js";
4
+ const IMPORT_SPECIFIER = `${PACKAGE_NAME}/antigravity`;
5
+ const EVENT_NAMES = Object.values(HOOK_FACTORY_TO_EVENT);
6
+ const EVENT_LOOKUP = new Map(EVENT_NAMES.map((event) => [event.toLowerCase(), event]));
7
+ const EVENT_TO_FACTORY = Object.fromEntries(Object.entries(HOOK_FACTORY_TO_EVENT).map(([factory, event]) => [event, factory]));
8
+ const EVENT_TO_OUTPUT = {
9
+ PreToolUse: "preToolUseOutput",
10
+ PostToolUse: "postToolUseOutput",
11
+ PreInvocation: "preInvocationOutput",
12
+ PostInvocation: "postInvocationOutput",
13
+ Stop: "stopOutput",
14
+ };
15
+ /** Resolves user-supplied hook names case-insensitively, rejecting the whole list on any miss. */
16
+ export function validateHookNames(requested) {
17
+ const normalized = [];
18
+ const rejected = [];
19
+ for (const name of requested) {
20
+ const event = EVENT_LOOKUP.get(name.toLowerCase());
21
+ if (event === undefined) {
22
+ rejected.push(name);
23
+ }
24
+ else {
25
+ normalized.push(event);
26
+ }
27
+ }
28
+ if (rejected.length > 0) {
29
+ return {
30
+ valid: false,
31
+ error: `Invalid hook names: ${rejected.join(", ")}. Valid names: ${EVENT_NAMES.join(", ")}`,
32
+ };
33
+ }
34
+ return { valid: true, normalized };
35
+ }
36
+ function kebab(value) {
37
+ return value
38
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
39
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
40
+ .toLowerCase();
41
+ }
42
+ function packageJson(projectName, outputPath) {
43
+ return `${JSON.stringify({
44
+ name: projectName,
45
+ version: "1.0.0",
46
+ type: "module",
47
+ scripts: {
48
+ build: `agent-hooks --agent antigravity -i "src/**/*.ts" -o "${outputPath}"`,
49
+ test: "vitest run",
50
+ lint: "biome check .",
51
+ typecheck: "tsc --noEmit",
52
+ },
53
+ dependencies: { [PACKAGE_NAME]: "^1.0.9" },
54
+ devDependencies: {
55
+ "@biomejs/biome": "2.4.9",
56
+ "@types/node": "^24.0.0",
57
+ typescript: "^5.9.3",
58
+ vitest: "^4.0.13",
59
+ },
60
+ engines: { node: ">=20.11.0" },
61
+ }, null, 2)}\n`;
62
+ }
63
+ function pluginJson(projectName) {
64
+ return `${JSON.stringify({
65
+ name: projectName,
66
+ version: "1.0.0",
67
+ description: `Antigravity hooks built with ${PACKAGE_NAME}`,
68
+ }, null, 2)}\n`;
69
+ }
70
+ function tsConfig() {
71
+ return `${JSON.stringify({
72
+ compilerOptions: {
73
+ target: "ES2022",
74
+ module: "NodeNext",
75
+ moduleResolution: "NodeNext",
76
+ strict: true,
77
+ esModuleInterop: true,
78
+ skipLibCheck: true,
79
+ },
80
+ include: ["src/**/*.ts", "test/**/*.ts"],
81
+ exclude: ["dist", "node_modules"],
82
+ }, null, 2)}\n`;
83
+ }
84
+ function biomeConfig() {
85
+ return `{
86
+ "$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
87
+ "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 },
88
+ "linter": { "enabled": true, "rules": { "recommended": true } },
89
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
90
+ "files": { "includes": ["src/**/*.ts", "test/**/*.ts", "*.ts"] }
91
+ }
92
+ `;
93
+ }
94
+ function vitestConfig() {
95
+ return `import { defineConfig } from "vitest/config";
96
+
97
+ export default defineConfig({
98
+ test: { include: ["test/**/*.test.ts"] },
99
+ });
100
+ `;
101
+ }
102
+ function readme(projectName, events) {
103
+ return `# ${projectName}
104
+
105
+ Antigravity hooks built with \`${PACKAGE_NAME}\`. This project includes: ${events.map((event) => `\`${event}\``).join(", ")}.
106
+
107
+ Run \`npm install\`, then \`npm run build\` to compile the hooks and write \`hooks.json\`. Antigravity reads that manifest from the plugin root and runs each command with its working directory set to the manifest's own directory, so the generated commands are relative and need no rewriting after install.
108
+
109
+ Every hook replies at exit 0: Antigravity has no exit-code channel, and each decision travels inside the JSON payload on stdout.
110
+ `;
111
+ }
112
+ function gitignore() {
113
+ return `bin/
114
+ dist/
115
+ node_modules/
116
+ *.log
117
+ `;
118
+ }
119
+ function hookSource(event) {
120
+ const factory = EVENT_TO_FACTORY[event];
121
+ const output = EVENT_TO_OUTPUT[event];
122
+ switch (event) {
123
+ case "PreToolUse":
124
+ return `import { ${factory}, ${output} } from "${IMPORT_SPECIFIER}";
125
+
126
+ export default ${factory}({ matcher: "run_command" }, (input) => {
127
+ // Antigravity gives tool arguments no fixed schema, so narrow before reading.
128
+ const commandLine = input.toolCall.args.CommandLine;
129
+ if (typeof commandLine === "string" && commandLine.includes("rm -rf")) {
130
+ return ${output}({ decision: "deny", reason: "Refusing a destructive shell command." });
131
+ }
132
+ return ${output}({ decision: "allow" });
133
+ });
134
+ `;
135
+ case "PostToolUse":
136
+ return `import { ${factory}, ${output} } from "${IMPORT_SPECIFIER}";
137
+
138
+ export default ${factory}({ matcher: "run_command" }, (input, { logger }) => {
139
+ // PostToolUse has no reply channel: the contract defines its output as {}.
140
+ // Report findings through the logger, never through stdout.
141
+ logger.info("Tool step finished", { stepIdx: input.stepIdx, error: input.error });
142
+ return ${output}();
143
+ });
144
+ `;
145
+ case "PreInvocation":
146
+ return `import { ${factory}, ${output} } from "${IMPORT_SPECIFIER}";
147
+
148
+ export default ${factory}({}, () => {
149
+ return ${output}({
150
+ injectSteps: [{ ephemeralMessage: "Check for lint errors before proposing changes." }],
151
+ });
152
+ });
153
+ `;
154
+ case "PostInvocation":
155
+ return `import { ${factory}, ${output} } from "${IMPORT_SPECIFIER}";
156
+
157
+ export default ${factory}({}, (input) => {
158
+ if (input.invocationNum < 2) {
159
+ return ${output}({ terminationBehavior: "force_continue" });
160
+ }
161
+ return ${output}({});
162
+ });
163
+ `;
164
+ case "Stop":
165
+ return `import { ${factory}, ${output} } from "${IMPORT_SPECIFIER}";
166
+
167
+ export default ${factory}({}, (input) => {
168
+ if (!input.fullyIdle) {
169
+ return ${output}({ decision: "continue", reason: "Background tasks are still running." });
170
+ }
171
+ return ${output}({});
172
+ });
173
+ `;
174
+ }
175
+ }
176
+ function hookTest(event) {
177
+ return `import { describe, expect, it } from "vitest";
178
+ import hook from "../src/${kebab(event)}.js";
179
+
180
+ describe("${event} hook", () => {
181
+ it("exports a ${event} hook", () => {
182
+ expect(hook.hookEventName).toBe("${event}");
183
+ });
184
+ });
185
+ `;
186
+ }
187
+ /** Creates a new Antigravity hook project. Refuses to write into an existing directory. */
188
+ export function scaffoldProject(options) {
189
+ const validation = validateHookNames(options.hooks);
190
+ if (!validation.valid) {
191
+ throw new Error(validation.error);
192
+ }
193
+ const { directory, outputPath } = options;
194
+ if (fs.existsSync(directory)) {
195
+ throw new Error(`Directory already exists: ${directory}`);
196
+ }
197
+ fs.mkdirSync(path.join(directory, "src"), { recursive: true });
198
+ fs.mkdirSync(path.join(directory, "test"), { recursive: true });
199
+ const projectName = path.basename(directory);
200
+ fs.writeFileSync(path.join(directory, "package.json"), packageJson(projectName, outputPath));
201
+ fs.writeFileSync(path.join(directory, "plugin.json"), pluginJson(projectName));
202
+ fs.writeFileSync(path.join(directory, "tsconfig.json"), tsConfig());
203
+ fs.writeFileSync(path.join(directory, "biome.json"), biomeConfig());
204
+ fs.writeFileSync(path.join(directory, "vitest.config.ts"), vitestConfig());
205
+ fs.writeFileSync(path.join(directory, ".gitignore"), gitignore());
206
+ fs.writeFileSync(path.join(directory, "README.md"), readme(projectName, validation.normalized));
207
+ for (const event of validation.normalized) {
208
+ fs.writeFileSync(path.join(directory, "src", `${kebab(event)}.ts`), hookSource(event));
209
+ fs.writeFileSync(path.join(directory, "test", `${kebab(event)}.test.ts`), hookTest(event));
210
+ }
211
+ }
@@ -3,7 +3,7 @@
3
3
  * `drive()`/`Transport` interface — the same seam Claude Code's and Codex's
4
4
  * transports are rewritten/re-exported against.
5
5
  *
6
- * Antigravity's wire semantics are the simplest of the three shipped agents,
6
+ * Antigravity's wire semantics are the simplest of the four shipped agents,
7
7
  * and also the least forgiving: **every reply exits 0, unconditionally.**
8
8
  * There is no exit-code channel at all — not the 2-of-30-events native-deny
9
9
  * split Claude Code has, not Codex's uniform stderr+exit-2 `BlockError`
@@ -16,6 +16,8 @@
16
16
  * ...fields }` on stdout at exit 0 — this is why the class is a stated
17
17
  * prerequisite (`outputs.ts` module docs, `events.ts` module docs): with no
18
18
  * exit-code fallback, a block computed mid-throw has nowhere else to land.
19
+ * The host acts on that decision for `PreToolUse`; on the other four events
20
+ * it is a well-formed reply carrying no decision the host recognizes.
19
21
  * - an unexpected handler/runtime failure → its stacktrace on stderr
20
22
  * (diagnostic only, not a wire signal) plus the empty response `{}` on
21
23
  * stdout, still at exit 0.
@@ -1,11 +1,18 @@
1
1
  /**
2
- * Antigravity wire types (plan step 5, item-6 descope: typed factories +
3
- * output builders + conformance matrix only — no `CONTRACT.md` pin, no
4
- * published protocol doc exists for this release). Shapes below are the
5
- * minimal surface the plan names for the five shipped events
6
- * (`PreToolUse`/`PostToolUse`/`PreInvocation`/`PostInvocation`/`Stop`),
7
- * derived structurally from Claude Code's and Codex's `types.ts` rather than
8
- * from any Antigravity-specific reference — there is none to cite yet.
2
+ * Antigravity wire types, transcribed from the host's own hook reference.
3
+ *
4
+ * Source of record: `assets/external/skills/agy-customizations/docs/hooks.md`,
5
+ * embedded in the `agy` binary and pinned verbatim in this directory's
6
+ * `CONTRACT.md`. Nothing here is derived by analogy from Claude Code or
7
+ * Codex — the three agents' payloads share no field.
8
+ *
9
+ * Two properties of this contract drive every shape below:
10
+ *
11
+ * 1. **All keys are camelCase** (protojson encoding), including `stepIdx` and
12
+ * `conversationId`.
13
+ * 2. **No payload carries its own event name.** Claude Code's
14
+ * `hook_event_name` and Codex's equivalent have no counterpart here; a
15
+ * handler knows its event only from the factory it was built with.
9
16
  * @module
10
17
  */
11
18
  export {};
@@ -55,7 +55,7 @@ function generatePackageJson(projectName, outputPath) {
55
55
  typecheck: "tsc --noEmit",
56
56
  },
57
57
  dependencies: {
58
- [PACKAGE_NAME]: "^1.0.7",
58
+ [PACKAGE_NAME]: "^1.0.9",
59
59
  },
60
60
  devDependencies: {
61
61
  "@biomejs/biome": "2.4.9",
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import { scaffoldProject } from "./scaffold.js";
26
26
  // ============================================================================
27
27
  // Constants
28
28
  // ============================================================================
29
- const VERSION = "1.0.7";
29
+ const VERSION = "1.0.9";
30
30
  const DEFAULT_ESBUILD_LOADERS = {
31
31
  ".md": "text",
32
32
  };
@@ -71,6 +71,10 @@ Required Arguments:
71
71
  bundle. There is no default: omitting this flag is an error.
72
72
  With --agent opencode, -o/--output is treated as a plugin artifact
73
73
  directory (no hooks.json manifest is generated).
74
+ With --agent antigravity, -o/--output is the hooks.json path at the
75
+ plugin root; bundles are written to a bin/ directory beside it and the
76
+ generated commands are relative to the manifest, which is the working
77
+ directory Antigravity runs each handler in.
74
78
  Example: --agent claude-code
75
79
 
76
80
  Build Mode (compile existing hooks):
@@ -94,6 +98,8 @@ Scaffold Mode (create new hook project):
94
98
  Valid types: PreToolUse, PostToolUse, PostToolUseFailure, Notification,
95
99
  UserPromptSubmit, SessionStart, SessionEnd, Stop,
96
100
  SubagentStart, SubagentStop, PreCompact, PermissionRequest
101
+ With --agent antigravity the valid types are: PreToolUse, PostToolUse,
102
+ PreInvocation, PostInvocation, Stop
97
103
  Example: --hooks Stop,SubagentStop,PreToolUse
98
104
 
99
105
  -o, --output <path>
@@ -1252,9 +1258,61 @@ async function main() {
1252
1258
  closeLog();
1253
1259
  }
1254
1260
  }
1255
- // Antigravity ships in step 5; fail closed until then.
1261
+ // Antigravity's manifest shape has no counterpart in the other agents: the
1262
+ // top-level keys are hook names, only two of the five events wrap their
1263
+ // handlers in a matcher group, and the host sets each handler's working
1264
+ // directory to the manifest's own directory. See
1265
+ // src/agents/antigravity/CONTRACT.md.
1266
+ if (args.agent === "antigravity") {
1267
+ const { runAntigravityCli, validateAntigravityArgs } = await import("./agents/antigravity/cli-support.js");
1268
+ const antigravityValidationError = validateAntigravityArgs({
1269
+ input: args.input,
1270
+ output: args.output,
1271
+ loaderFlags: args.loaderFlags ?? [],
1272
+ sourcemap: args.sourcemap,
1273
+ stableNames: args.stableNames,
1274
+ scaffold: args.scaffold,
1275
+ hooks: args.hooks,
1276
+ });
1277
+ if (antigravityValidationError !== undefined) {
1278
+ process.stderr.write(`Error: ${antigravityValidationError}\n\n`);
1279
+ process.stdout.write(HELP_TEXT);
1280
+ process.exit(1);
1281
+ }
1282
+ try {
1283
+ const compiledCount = await runAntigravityCli({
1284
+ input: args.input,
1285
+ output: args.output,
1286
+ executable: args.executable,
1287
+ loaderFlags: args.loaderFlags ?? [],
1288
+ sourcemap: args.sourcemap,
1289
+ stableNames: args.stableNames,
1290
+ scaffold: args.scaffold,
1291
+ hooks: args.hooks,
1292
+ });
1293
+ if (args.scaffold !== undefined && args.scaffold !== "") {
1294
+ process.stdout.write(`Scaffolded ${path.resolve(process.cwd(), args.scaffold)}\n`);
1295
+ }
1296
+ else {
1297
+ process.stdout.write(`Generated ${path.resolve(process.cwd(), args.output)} with ${compiledCount} hook(s)\n`);
1298
+ }
1299
+ process.exit(0);
1300
+ }
1301
+ catch (error) {
1302
+ const message = error instanceof Error ? error.message : String(error);
1303
+ log("error", "Antigravity build failed", { error: message });
1304
+ process.stderr.write(`Error: ${message}\n`);
1305
+ process.exit(1);
1306
+ }
1307
+ finally {
1308
+ closeLog();
1309
+ }
1310
+ }
1311
+ // Everything below is the Claude Code path. Each other agent returns from
1312
+ // its own branch above, so reaching here with a different agent would mean
1313
+ // a branch was removed without removing its VALID_AGENTS entry.
1256
1314
  if (args.agent !== "claude-code") {
1257
- process.stderr.write(`Error: --agent ${args.agent} is not implemented in this release; only "claude-code", "codex", and "opencode" build today (antigravity ships in step 5).\n`);
1315
+ process.stderr.write(`Error: --agent ${args.agent} has no build branch.\n`);
1258
1316
  process.exit(1);
1259
1317
  }
1260
1318
  // Handle scaffold mode
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * {@link HookBlockError}, the Logger, env utilities, and stdin helpers.
7
7
  *
8
8
  * Per-agent entry points (`@goodfoot/agent-hooks/claude-code`, `/codex`,
9
- * `/antigravity`) are added to the package `exports` map only in the step
10
- * that lands their implementation — never pre-declared.
9
+ * `/opencode`, `/antigravity`) expose each host's native protocol without
10
+ * leaking host-specific types through this root module.
11
11
  * @module
12
12
  */
13
13
  export { defineHook } from "./core/define-hook.js";
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.7",
134
+ "@goodfoot/agent-hooks": "^1.0.9",
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.7",
3
+ "version": "1.0.9",
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": {
@@ -43,6 +43,10 @@
43
43
  "./opencode": {
44
44
  "import": "./dist/agents/opencode/index.js",
45
45
  "types": "./types/agents/opencode/index.d.ts"
46
+ },
47
+ "./antigravity": {
48
+ "import": "./dist/agents/antigravity/index.js",
49
+ "types": "./types/agents/antigravity/index.d.ts"
46
50
  }
47
51
  },
48
52
  "bin": "./dist/cli.js",
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Antigravity-specific CLI internals: source analysis, bundle compilation
3
+ * linking the Antigravity runtime module, and `hooks.json` generation in the
4
+ * host's own named-hook shape.
5
+ *
6
+ * The manifest shape here has no counterpart in the other agents. Claude Code
7
+ * and Codex both emit `{ "hooks": { "<Event>": [...] } }`; Antigravity's
8
+ * top-level keys are **hook names**, each mapping to its own event table, and
9
+ * only two of the five events wrap their handlers in a `{ matcher, hooks }`
10
+ * group. The host also sets each handler's working directory to the directory
11
+ * containing `hooks.json`, so commands are emitted relative to it and need no
12
+ * `${PLUGIN_ROOT}` substitution or git-toplevel escape. See `CONTRACT.md` in
13
+ * this directory for the pinned reference all of that is transcribed from.
14
+ *
15
+ * Driver-level primitives (loader parsing, glob discovery, content hashing,
16
+ * symlink re-rooting, stale-bundle pruning) are imported from the shared CLI
17
+ * module — never re-derived here.
18
+ * @module
19
+ */
20
+ import * as esbuild from "esbuild";
21
+ import type { HookEventName } from "./types.js";
22
+ /** Arguments the `--agent antigravity` branch accepts. */
23
+ export interface AntigravityCliArgs {
24
+ input: string;
25
+ output: string;
26
+ executable?: string;
27
+ loaderFlags: string[];
28
+ sourcemap?: boolean;
29
+ stableNames?: boolean;
30
+ }
31
+ /** What the analyzer recovers from one hook source file. */
32
+ export interface AntigravityHookMetadata {
33
+ hookEventName: HookEventName;
34
+ matcher?: string;
35
+ /** As written in the source config, in milliseconds. Converted to seconds on the wire. */
36
+ timeout?: number;
37
+ }
38
+ interface CompiledAntigravityHook {
39
+ sourcePath: string;
40
+ outputPath: string;
41
+ hookName: string;
42
+ metadata: AntigravityHookMetadata;
43
+ }
44
+ /** One handler entry, as it appears in `hooks.json`. */
45
+ export interface AntigravityHandler {
46
+ type: "command";
47
+ command: string;
48
+ timeout: number;
49
+ }
50
+ /** A grouped entry, used by `PreToolUse` and `PostToolUse` only. */
51
+ export interface AntigravityMatcherGroup {
52
+ matcher: string;
53
+ hooks: AntigravityHandler[];
54
+ }
55
+ /**
56
+ * One named hook's event table. Grouped events carry
57
+ * {@link AntigravityMatcherGroup} entries; flat events carry handlers
58
+ * directly.
59
+ */
60
+ export type AntigravityNamedHook = {
61
+ [Event in HookEventName]?: Event extends "PreToolUse" | "PostToolUse" ? AntigravityMatcherGroup[] : AntigravityHandler[];
62
+ };
63
+ /** The whole manifest: hook name to event table. */
64
+ export type AntigravityHooksJson = Record<string, AntigravityNamedHook>;
65
+ /**
66
+ * Recovers the bound event, matcher, and timeout from a hook module's default
67
+ * export. Returns `undefined` when the default export is not a call to a
68
+ * known Antigravity factory, which is how non-hook files in the input glob
69
+ * are skipped.
70
+ */
71
+ export declare function analyzeAntigravityHookFile(sourcePath: string): AntigravityHookMetadata | undefined;
72
+ /**
73
+ * Bundles one hook source into a self-contained ESM executable whose entry
74
+ * wrapper calls this agent's `execute`.
75
+ */
76
+ export declare function compileAntigravityHook(sourcePath: string, loaders: Record<string, esbuild.Loader>, sourcemap?: boolean): Promise<{
77
+ content: string;
78
+ contentHash: string;
79
+ }>;
80
+ /**
81
+ * Builds the manifest. Each compiled hook becomes one top-level named entry
82
+ * holding exactly one event, which is what the host merges across plugins.
83
+ */
84
+ export declare function generateAntigravityHooksJson(compiled: CompiledAntigravityHook[], manifestPath: string, executable?: string): AntigravityHooksJson;
85
+ /** Validates the Antigravity build arguments beyond the shared `--agent` gate. */
86
+ export declare function validateAntigravityArgs(args: AntigravityCliArgs & {
87
+ scaffold?: string;
88
+ hooks?: string;
89
+ }): string | undefined;
90
+ /**
91
+ * Runs the Antigravity build or scaffold branch. Bundle filenames are stable
92
+ * by default: a manifest committed into a plugin tree must not churn on every
93
+ * rebuild, and the host reads the command string rather than globbing the
94
+ * directory.
95
+ */
96
+ export declare function runAntigravityCli(args: AntigravityCliArgs & {
97
+ scaffold?: string;
98
+ hooks?: string;
99
+ }): Promise<number>;
100
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { HookEventName } from "./types.js";
2
+ export declare const PACKAGE_NAME = "@goodfoot/agent-hooks";
3
+ /** The host's own default handler timeout, in seconds (`CONTRACT.md`, Hook Handler Fields). */
4
+ export declare const DEFAULT_TIMEOUT_SECONDS = 30;
5
+ export declare const DEFAULT_ESBUILD_LOADERS: {
6
+ readonly ".md": "text";
7
+ };
8
+ /** Maps each exported factory name to the event it binds. */
9
+ export declare const HOOK_FACTORY_TO_EVENT: Record<string, HookEventName>;
10
+ /**
11
+ * Events whose `hooks.json` entry is a grouped `{ matcher, hooks }` wrapper.
12
+ * The remaining three are flat lists of handler objects; the host ignores a
13
+ * matcher on them.
14
+ */
15
+ export declare const GROUPED_EVENTS: Set<HookEventName>;