@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.
- package/CHANGELOG.md +13 -0
- package/README.md +67 -34
- package/dist/agents/antigravity/cli-support.js +284 -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/cli-support.js +4 -2
- package/dist/agents/codex/scaffold.js +1 -1
- package/dist/agents/opencode/cli-support.js +1 -1
- package/dist/cli.js +92 -10
- 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 +16 -5
- 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.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
|
|
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 {};
|
|
@@ -15,7 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import * as esbuild from "esbuild";
|
|
17
17
|
import ts from "typescript";
|
|
18
|
-
import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
|
|
18
|
+
import { buildEsbuildLoaderMap, discoverHookSourceFiles, moduleWorkingDir, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
|
|
19
19
|
import { EVENTS_WITH_MATCHER, HOOK_FACTORY_TO_EVENT } from "./constants.js";
|
|
20
20
|
/**
|
|
21
21
|
* Extracts Codex hook metadata (event name, matcher, timeout in
|
|
@@ -88,9 +88,10 @@ export function analyzeCodexHookFile(sourcePath) {
|
|
|
88
88
|
* Compiles one Codex hook to a self-contained ESM executable whose entry
|
|
89
89
|
* wrapper imports this package's Codex runtime module (`execute`).
|
|
90
90
|
*/
|
|
91
|
-
export async function compileCodexHook(sourcePath, loaders, sourcemap =
|
|
91
|
+
export async function compileCodexHook(sourcePath, loaders, sourcemap = false) {
|
|
92
92
|
const resolveDir = path.dirname(sourcePath);
|
|
93
93
|
const runtimePathAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./transport.js"), resolveDir);
|
|
94
|
+
const absWorkingDir = moduleWorkingDir(runtimePathAbsolute, resolveDir);
|
|
94
95
|
let relativeRuntimePath = path.relative(resolveDir, runtimePathAbsolute).replace(/\\/g, "/");
|
|
95
96
|
if (!relativeRuntimePath.startsWith(".")) {
|
|
96
97
|
relativeRuntimePath = `./${relativeRuntimePath}`;
|
|
@@ -102,6 +103,7 @@ execute(hook);
|
|
|
102
103
|
`;
|
|
103
104
|
const baseName = path.basename(sourcePath, path.extname(sourcePath));
|
|
104
105
|
const result = await esbuild.build({
|
|
106
|
+
absWorkingDir,
|
|
105
107
|
stdin: {
|
|
106
108
|
contents: wrapperContent,
|
|
107
109
|
resolveDir,
|
|
@@ -23,7 +23,7 @@ import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag
|
|
|
23
23
|
* Bundles one OpenCode plugin entry to a self-contained ESM artifact. The
|
|
24
24
|
* source file's own default export is preserved unchanged — no wrapper.
|
|
25
25
|
*/
|
|
26
|
-
export async function compileOpenCodePlugin(sourcePath, loaders, sourcemap =
|
|
26
|
+
export async function compileOpenCodePlugin(sourcePath, loaders, sourcemap = false) {
|
|
27
27
|
const result = await esbuild.build({
|
|
28
28
|
entryPoints: [sourcePath],
|
|
29
29
|
loader: loaders,
|
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ import { scaffoldProject } from "./scaffold.js";
|
|
|
26
26
|
// ============================================================================
|
|
27
27
|
// Constants
|
|
28
28
|
// ============================================================================
|
|
29
|
-
const VERSION = "1.0.
|
|
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
|
|
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
|
|
145
|
-
|
|
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:
|
|
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
|
|
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
|
|
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}
|
|
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`)
|
|
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.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.
|
|
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",
|