@goodfoot/agent-hooks 1.0.8 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +65 -34
- package/dist/agents/antigravity/cli-support.js +282 -0
- package/dist/agents/antigravity/constants.js +20 -0
- package/dist/agents/antigravity/events.js +24 -22
- package/dist/agents/antigravity/hooks.js +7 -8
- package/dist/agents/antigravity/index.js +7 -18
- package/dist/agents/antigravity/outputs.js +38 -31
- package/dist/agents/antigravity/scaffold.js +211 -0
- package/dist/agents/antigravity/transport.js +3 -1
- package/dist/agents/antigravity/types.js +14 -7
- package/dist/agents/codex/scaffold.js +1 -1
- package/dist/cli.js +61 -3
- package/dist/index.js +2 -2
- package/dist/scaffold.js +1 -1
- package/package.json +5 -1
- package/types/agents/antigravity/cli-support.d.ts +100 -0
- package/types/agents/antigravity/constants.d.ts +15 -0
- package/types/agents/antigravity/events.d.ts +24 -22
- package/types/agents/antigravity/hooks.d.ts +7 -8
- package/types/agents/antigravity/index.d.ts +9 -16
- package/types/agents/antigravity/outputs.d.ts +94 -65
- package/types/agents/antigravity/scaffold.d.ts +16 -0
- package/types/agents/antigravity/transport.d.ts +3 -1
- package/types/agents/antigravity/types.d.ts +53 -26
- package/types/cli.d.ts +1 -1
- package/types/index.d.ts +2 -2
|
@@ -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
|
-
*
|
|
19
|
-
* Antigravity
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
systemMessage: options.systemMessage,
|
|
62
|
+
permissionOverrides: options.permissionOverrides,
|
|
63
|
+
overwrite: options.overwrite,
|
|
51
64
|
});
|
|
52
65
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {};
|
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.
|
|
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
|
|
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}
|
|
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`)
|
|
10
|
-
*
|
|
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.
|
|
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.
|
|
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>;
|