@goodfoot/agent-hooks 1.0.8 → 1.0.10

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.10" },
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 {};
@@ -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.8",
58
+ [PACKAGE_NAME]: "^1.0.10",
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,7 @@ import { scaffoldProject } from "./scaffold.js";
26
26
  // ============================================================================
27
27
  // Constants
28
28
  // ============================================================================
29
- const VERSION = "1.0.8";
29
+ const VERSION = "1.0.10";
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>
@@ -136,13 +142,13 @@ Optional Arguments:
136
142
  --no-stable-names
137
143
  Restore the pre-1.7 behavior: hashed compiled bundles (<name>.<hash>.mjs).
138
144
 
139
- --sourcemap (default)
145
+ --sourcemap
140
146
  Embed an inline sourcemap in each compiled bundle so stack traces show
141
147
  original TypeScript source locations.
142
148
 
143
- --no-sourcemap
144
- Emit bundles without the inline sourcemap, shrinking compiled output by
145
- roughly 85-90%. The content hash is unaffected.
149
+ --no-sourcemap (default)
150
+ Emit bundles without the inline sourcemap, shrinking compiled output.
151
+ The content hash is unaffected.
146
152
 
147
153
  -h, --help
148
154
  Show this help message.
@@ -251,7 +257,7 @@ function parseArgs(argv) {
251
257
  // as true, while the Codex branch defaults by command context
252
258
  // (plugin mode → stable, otherwise hashed).
253
259
  stableNames: undefined,
254
- sourcemap: true,
260
+ sourcemap: false,
255
261
  };
256
262
  for (let i = 0; i < argv.length; i++) {
257
263
  const arg = argv[i];
@@ -636,6 +642,28 @@ function symlinkVisiblePath(realPath, resolveDir) {
636
642
  dir = parent;
637
643
  }
638
644
  }
645
+ /**
646
+ * Returns the stable project anchor for a module reached through node_modules.
647
+ *
648
+ * esbuild otherwise uses process.cwd() when rendering module-boundary comments
649
+ * and sourcemap source paths. A workspace script normally runs from the
650
+ * consuming package, while its dependencies may be hoisted several directories
651
+ * higher, making those emitted paths depend on the consumer's nesting depth.
652
+ * Anchoring at the directory which owns the visible node_modules keeps the
653
+ * canonical `node_modules/...` form in every such layout.
654
+ */
655
+ function moduleWorkingDir(modulePath, fallback) {
656
+ for (let dir = path.dirname(modulePath);;) {
657
+ if (path.basename(dir) === "node_modules") {
658
+ return path.dirname(dir);
659
+ }
660
+ const parent = path.dirname(dir);
661
+ if (parent === dir) {
662
+ return fallback;
663
+ }
664
+ dir = parent;
665
+ }
666
+ }
639
667
  /**
640
668
  * Compiles a TypeScript hook file to a self-contained ESM executable.
641
669
  *
@@ -666,6 +694,7 @@ async function compileHook(options) {
666
694
  // nesting depth.
667
695
  const resolveDir = path.dirname(sourcePath);
668
696
  const runtimePathAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./agents/claude-code/transport.js"), resolveDir);
697
+ const absWorkingDir = moduleWorkingDir(runtimePathAbsolute, resolveDir);
669
698
  // Compute relative paths from resolveDir to avoid absolute paths in source maps.
670
699
  // This ensures reproducible builds regardless of checkout directory.
671
700
  const relativeSourcePath = `./${path.basename(sourcePath)}`;
@@ -725,6 +754,7 @@ execute(hook);
725
754
  ].join("\n");
726
755
  // Common esbuild options
727
756
  const commonOptions = {
757
+ absWorkingDir,
728
758
  stdin: stdinOptions,
729
759
  loader: loaders,
730
760
  format: "esm",
@@ -778,7 +808,7 @@ execute(hook);
778
808
  // output minus the trailing sourceMappingURL comment (verified on esbuild
779
809
  // 0.24.2), so the sourcemap-free content is emitted directly. The content
780
810
  // hash is always derived from pass-1 output, so it is unchanged by the flag.
781
- if (sourcemap === false) {
811
+ if (sourcemap !== true) {
782
812
  return { content: contentForHash, contentHash };
783
813
  }
784
814
  const resultWithSourcemap = await esbuild.build({
@@ -1252,9 +1282,61 @@ async function main() {
1252
1282
  closeLog();
1253
1283
  }
1254
1284
  }
1255
- // Antigravity ships in step 5; fail closed until then.
1285
+ // Antigravity's manifest shape has no counterpart in the other agents: the
1286
+ // top-level keys are hook names, only two of the five events wrap their
1287
+ // handlers in a matcher group, and the host sets each handler's working
1288
+ // directory to the manifest's own directory. See
1289
+ // src/agents/antigravity/CONTRACT.md.
1290
+ if (args.agent === "antigravity") {
1291
+ const { runAntigravityCli, validateAntigravityArgs } = await import("./agents/antigravity/cli-support.js");
1292
+ const antigravityValidationError = validateAntigravityArgs({
1293
+ input: args.input,
1294
+ output: args.output,
1295
+ loaderFlags: args.loaderFlags ?? [],
1296
+ sourcemap: args.sourcemap,
1297
+ stableNames: args.stableNames,
1298
+ scaffold: args.scaffold,
1299
+ hooks: args.hooks,
1300
+ });
1301
+ if (antigravityValidationError !== undefined) {
1302
+ process.stderr.write(`Error: ${antigravityValidationError}\n\n`);
1303
+ process.stdout.write(HELP_TEXT);
1304
+ process.exit(1);
1305
+ }
1306
+ try {
1307
+ const compiledCount = await runAntigravityCli({
1308
+ input: args.input,
1309
+ output: args.output,
1310
+ executable: args.executable,
1311
+ loaderFlags: args.loaderFlags ?? [],
1312
+ sourcemap: args.sourcemap,
1313
+ stableNames: args.stableNames,
1314
+ scaffold: args.scaffold,
1315
+ hooks: args.hooks,
1316
+ });
1317
+ if (args.scaffold !== undefined && args.scaffold !== "") {
1318
+ process.stdout.write(`Scaffolded ${path.resolve(process.cwd(), args.scaffold)}\n`);
1319
+ }
1320
+ else {
1321
+ process.stdout.write(`Generated ${path.resolve(process.cwd(), args.output)} with ${compiledCount} hook(s)\n`);
1322
+ }
1323
+ process.exit(0);
1324
+ }
1325
+ catch (error) {
1326
+ const message = error instanceof Error ? error.message : String(error);
1327
+ log("error", "Antigravity build failed", { error: message });
1328
+ process.stderr.write(`Error: ${message}\n`);
1329
+ process.exit(1);
1330
+ }
1331
+ finally {
1332
+ closeLog();
1333
+ }
1334
+ }
1335
+ // Everything below is the Claude Code path. Each other agent returns from
1336
+ // its own branch above, so reaching here with a different agent would mean
1337
+ // a branch was removed without removing its VALID_AGENTS entry.
1256
1338
  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`);
1339
+ process.stderr.write(`Error: --agent ${args.agent} has no build branch.\n`);
1258
1340
  process.exit(1);
1259
1341
  }
1260
1342
  // Handle scaffold mode
@@ -1389,4 +1471,4 @@ if (isDirectExecution) {
1389
1471
  // Shared build primitives reused by the per-agent CLI branches (single
1390
1472
  // source for driver logic — the duplication check forbids re-deriving these
1391
1473
  // inside agents/*).
1392
- 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, };
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, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
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.8",
134
+ "@goodfoot/agent-hooks": "^1.0.10",
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.8",
3
+ "version": "1.0.10",
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",