@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @goodfoot/agent-hooks npm package changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.9
|
|
4
|
+
- Added support for Antigravity hooks, including event handling, output formatting, and transport
|
|
5
|
+
- Added scaffolding for Antigravity hook projects via the CLI
|
|
6
|
+
- Documented the Antigravity hook contract in the README
|
|
7
|
+
|
|
8
|
+
## 1.0.9
|
|
9
|
+
|
|
10
|
+
Adds full Antigravity support to the public package: typed hook factories and outputs, transport behavior, the `@goodfoot/agent-hooks/antigravity` export, CLI compilation, and a scaffold that emits an installable Antigravity plugin manifest. The implementation is covered by unit, conformance, export-surface, scaffold, and live host smoke tests.
|
|
11
|
+
|
|
3
12
|
## 1.0.8
|
|
4
13
|
|
|
5
14
|
Maintenance release accompanying a repository version-bump hook fix: a version surface is now exempt only for its version, not for its whole contents, so an edit to a manifest or a version-bearing source file can no longer ship unversioned. No changes to the hook API, the compiler, or the CLI.
|
package/README.md
CHANGED
|
@@ -6,17 +6,15 @@
|
|
|
6
6
|
`@goodfoot/codex-hooks`: a shared agent-neutral core (hook factory primitive, transport
|
|
7
7
|
driver, `Logger`, env/stdin utilities) with a per-agent entry point on top. Write
|
|
8
8
|
TypeScript hooks, compile them with the bundled CLI, and run the resulting
|
|
9
|
-
self-contained executables under Claude Code or
|
|
9
|
+
self-contained executables under Claude Code, Codex, OpenCode, or Antigravity.
|
|
10
10
|
|
|
11
|
-
**Status: `1.0.
|
|
11
|
+
**Status: `1.0.8`.** Four host entry points ship: `@goodfoot/agent-hooks/claude-code`
|
|
12
12
|
(full parity with `@goodfoot/claude-code-hooks`), `@goodfoot/agent-hooks/codex` (full
|
|
13
|
-
parity with `@goodfoot/codex-hooks`),
|
|
14
|
-
for OpenCode's in-process plugin model — see [OpenCode](#opencode) below)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
the CLI. It ships once a non-interactive, authenticated Antigravity CLI exists to
|
|
19
|
-
validate against.
|
|
13
|
+
parity with `@goodfoot/codex-hooks`), `@goodfoot/agent-hooks/opencode` (native support
|
|
14
|
+
for OpenCode's in-process plugin model — see [OpenCode](#opencode) below), and
|
|
15
|
+
`@goodfoot/agent-hooks/antigravity` (five typed events, host-native output builders,
|
|
16
|
+
manifest generation, scaffolding, and an exit-0 payload transport). The Antigravity
|
|
17
|
+
surface is pinned to and behaviorally verified against `agy` 1.1.22.
|
|
20
18
|
|
|
21
19
|
## Install
|
|
22
20
|
|
|
@@ -35,9 +33,10 @@ yarn add @goodfoot/agent-hooks
|
|
|
35
33
|
### The plugin, for development
|
|
36
34
|
|
|
37
35
|
All four hosts install from
|
|
38
|
-
[`goodfoot-io/marketplace`](https://github.com/goodfoot-io/marketplace). Claude Code
|
|
39
|
-
Codex
|
|
40
|
-
|
|
36
|
+
[`goodfoot-io/marketplace`](https://github.com/goodfoot-io/marketplace). Claude Code and
|
|
37
|
+
Codex add the repository as a marketplace and install by name. Antigravity installs one
|
|
38
|
+
plugin root at a time. OpenCode needs a checkout, because these plugin packages are not
|
|
39
|
+
published to npm.
|
|
41
40
|
|
|
42
41
|
Note that the plugin and the npm package version independently — the plugin version you
|
|
43
42
|
install will not match the package version above.
|
|
@@ -50,6 +49,8 @@ claude plugin install agent-hooks@goodfoot
|
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
Verify with `claude plugin list` — `agent-hooks@goodfoot` should be listed as enabled.
|
|
52
|
+
`claude plugin details agent-hooks` reports the component inventory, confirming the skill
|
|
53
|
+
itself arrived rather than just the plugin.
|
|
53
54
|
|
|
54
55
|
#### Codex
|
|
55
56
|
|
|
@@ -59,48 +60,71 @@ codex plugin add agent-hooks@goodfoot
|
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
The marketplace source must be a repository, git URL, or directory; passing the manifest
|
|
62
|
-
path itself is rejected.
|
|
63
|
-
|
|
63
|
+
path itself is rejected. A bare plugin name is rejected even when exactly one marketplace
|
|
64
|
+
is configured — qualify it as `agent-hooks@goodfoot` or pass `--marketplace goodfoot`. If
|
|
65
|
+
you set `CODEX_HOME`, create that directory first; Codex will not create it and fails with
|
|
66
|
+
`failed to resolve CODEX_HOME`.
|
|
67
|
+
|
|
68
|
+
Verify with `codex plugin list`, which lists every marketplace plugin with a `STATUS`
|
|
69
|
+
column. Do not use `codex plugin list --json` for this: on codex-cli 0.150.1 its
|
|
70
|
+
`available` array is empty even when the plain listing shows all seven plugins, so a
|
|
71
|
+
correctly installed plugin can look missing. The skill tree lands under
|
|
64
72
|
`$CODEX_HOME/plugins/cache/goodfoot/agent-hooks/<version>/skills`.
|
|
65
73
|
|
|
66
74
|
#### Antigravity
|
|
67
75
|
|
|
68
|
-
`agy`
|
|
69
|
-
with the in-repo path appended:
|
|
76
|
+
`agy` installs one plugin root at a time, from a git URL with the in-repo path appended:
|
|
70
77
|
|
|
71
78
|
```bash
|
|
72
79
|
agy plugin install https://github.com/goodfoot-io/marketplace.git/plugins-antigravity/agent-hooks
|
|
73
80
|
```
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
There is no marketplace form that installs several at once: pointing `agy plugin install`
|
|
83
|
+
at the repository root is rejected with `could not detect plugin structure`, as is the
|
|
84
|
+
`#subdir` fragment form other tools accept. A local path works the same way: `agy plugin
|
|
85
|
+
install ./plugins-antigravity/agent-hooks`.
|
|
86
|
+
|
|
87
|
+
To add the sibling plugins, repeat the command for each root you want —
|
|
88
|
+
`plugins-antigravity/` carries `agent-hooks`, `agent-skills`, `claude-code-skill-reader`,
|
|
89
|
+
`gmail`, `goodfoot`, `jsdoczoom`, and `linear`.
|
|
78
90
|
|
|
79
91
|
A successful install reports `skills : 1 processed`; `agents`, `commands`, `mcpServers`,
|
|
80
92
|
and `hooks` report `skipped (not found)`, expected for a skills-only plugin. Files land
|
|
81
|
-
in `~/.gemini/config/plugins/agent-hooks/`. Verify with `agy plugin list
|
|
93
|
+
in `~/.gemini/config/plugins/agent-hooks/`. Verify with `agy plugin list`, which reports
|
|
94
|
+
`"components": ["skills"]` for the installed plugin.
|
|
82
95
|
|
|
83
|
-
This installs the authoring skill only.
|
|
84
|
-
|
|
96
|
+
This marketplace plugin installs the authoring skill only. Hook projects use the npm
|
|
97
|
+
package separately: import `@goodfoot/agent-hooks/antigravity` and compile with
|
|
98
|
+
`agent-hooks --agent antigravity`. The compiler writes the host-native `hooks.json` at
|
|
99
|
+
the plugin root plus its executable bundles under `bin/`; install that resulting plugin
|
|
100
|
+
root with `agy plugin install <dir>`. Scaffolding an Antigravity hook project also emits
|
|
101
|
+
the root `plugin.json` required by that install command.
|
|
85
102
|
|
|
86
103
|
#### OpenCode
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
plugin is not a way to get the skill. Clone the repository and register the skill
|
|
91
|
-
directory in `opencode.json`:
|
|
105
|
+
These plugin packages are not published to npm, so `opencode plugin` takes a path into a
|
|
106
|
+
checkout rather than a module name:
|
|
92
107
|
|
|
93
108
|
```bash
|
|
94
109
|
git clone https://github.com/goodfoot-io/marketplace.git
|
|
110
|
+
opencode plugin "$PWD/marketplace/plugins-opencode/agent-hooks"
|
|
95
111
|
```
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
Pass a path, not a bare name. `opencode plugin` resolves a specifier containing a
|
|
114
|
+
separator as a path — absolute, `./`-prefixed, and plain relative forms all load on
|
|
115
|
+
OpenCode 1.18.23. A single segment with no separator (`agent-hooks`) is instead resolved as an
|
|
116
|
+
npm package name and silently fails to load, with nothing logged. The absolute form
|
|
117
|
+
above avoids the ambiguity.
|
|
118
|
+
|
|
119
|
+
That is the whole install — no `opencode.json` editing. `opencode plugin` writes only the
|
|
120
|
+
top-level `plugin` key, and OpenCode keeps `plugin` and `skills` as disjoint config keys,
|
|
121
|
+
so registering the module would not by itself surface its skills. The plugin closes that
|
|
122
|
+
gap from the inside: its `config` hook runs against the resolved config before use and
|
|
123
|
+
appends its own bundled `skills/` directory to `skills.paths`, resolved relative to
|
|
124
|
+
wherever the package was installed. Installing is enough.
|
|
125
|
+
|
|
126
|
+
Without flags this installs into the current project's `.opencode/opencode.json`. Add
|
|
127
|
+
`-g` to install into the global config instead.
|
|
104
128
|
|
|
105
129
|
Verify with `opencode debug skill`. Its output is large and embeds whole skill bodies:
|
|
106
130
|
redirect it to a file and search there. Piping it to `head` truncates the JSON mid-string
|
|
@@ -119,7 +143,8 @@ Re-running the install command does not move the version on every host.
|
|
|
119
143
|
- **Codex** — `codex plugin marketplace upgrade goodfoot` moves the installed version;
|
|
120
144
|
there is no separate plugin-update command.
|
|
121
145
|
- **Antigravity** — re-run `agy plugin install`.
|
|
122
|
-
- **OpenCode** — pull the checkout
|
|
146
|
+
- **OpenCode** — pull the checkout; the plugin resolves its skills from that directory, so
|
|
147
|
+
no reinstall is needed. Re-run with `-f` to replace a pinned plugin version.
|
|
123
148
|
|
|
124
149
|
Check the installed version rather than the exit code. Several hosts report success while
|
|
125
150
|
leaving a stale version in place, and a newer directory in a host's cache proves only that
|
|
@@ -251,6 +276,12 @@ export default defineOpenCodePlugin({
|
|
|
251
276
|
`observe`/`observeResumed` record first-seen-event distinctly and `isRoot`/`isResumed`
|
|
252
277
|
answer from that record.
|
|
253
278
|
|
|
279
|
+
`Hooks` also includes `config?: (input: Config) => Promise<void>`, which runs against the
|
|
280
|
+
resolved config before use and can mutate it. That is how a plugin contributes
|
|
281
|
+
configuration it would otherwise need the user to hand-write — the plugins in this
|
|
282
|
+
repository use it to append their own `skills/` directory to `skills.paths`, so installing
|
|
283
|
+
one delivers its skills without any `opencode.json` edit.
|
|
284
|
+
|
|
254
285
|
Compile with the same unified CLI, but `--agent opencode` treats `-o`/`--output` as a
|
|
255
286
|
plugin-artifact **directory**, not a manifest path — OpenCode config references built
|
|
256
287
|
files directly, so there is no `hooks.json`-equivalent manifest or shebang wrapper:
|
|
@@ -0,0 +1,282 @@
|
|
|
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 fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import * as esbuild from "esbuild";
|
|
24
|
+
import ts from "typescript";
|
|
25
|
+
import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
|
|
26
|
+
import { DEFAULT_TIMEOUT_SECONDS, GROUPED_EVENTS, HOOK_FACTORY_TO_EVENT } from "./constants.js";
|
|
27
|
+
import { HOOK_EVENT_NAMES } from "./events.js";
|
|
28
|
+
/** The matcher emitted when a grouped event's config names none. `"*"` matches every tool. */
|
|
29
|
+
const MATCH_ALL = "*";
|
|
30
|
+
/**
|
|
31
|
+
* Recovers the bound event, matcher, and timeout from a hook module's default
|
|
32
|
+
* export. Returns `undefined` when the default export is not a call to a
|
|
33
|
+
* known Antigravity factory, which is how non-hook files in the input glob
|
|
34
|
+
* are skipped.
|
|
35
|
+
*/
|
|
36
|
+
export function analyzeAntigravityHookFile(sourcePath) {
|
|
37
|
+
const sourceFile = ts.createSourceFile(sourcePath, fs.readFileSync(sourcePath, "utf-8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
38
|
+
const unwrap = (node) => {
|
|
39
|
+
if (ts.isParenthesizedExpression(node) || ts.isAwaitExpression(node)) {
|
|
40
|
+
return unwrap(node.expression);
|
|
41
|
+
}
|
|
42
|
+
return ts.isCallExpression(node) ? node : undefined;
|
|
43
|
+
};
|
|
44
|
+
const factoryOf = (call) => {
|
|
45
|
+
if (ts.isIdentifier(call.expression)) {
|
|
46
|
+
return call.expression.text;
|
|
47
|
+
}
|
|
48
|
+
return ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : undefined;
|
|
49
|
+
};
|
|
50
|
+
const literalString = (node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
|
|
51
|
+
let found;
|
|
52
|
+
const inspect = (node) => {
|
|
53
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
54
|
+
const call = unwrap(node.expression);
|
|
55
|
+
const factory = call === undefined ? undefined : factoryOf(call);
|
|
56
|
+
const hookEventName = factory === undefined ? undefined : HOOK_FACTORY_TO_EVENT[factory];
|
|
57
|
+
if (call !== undefined && hookEventName !== undefined) {
|
|
58
|
+
const result = { hookEventName };
|
|
59
|
+
const [config] = call.arguments;
|
|
60
|
+
if (config !== undefined && ts.isObjectLiteralExpression(config)) {
|
|
61
|
+
for (const property of config.properties) {
|
|
62
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (property.name.text === "matcher") {
|
|
66
|
+
result.matcher = literalString(property.initializer);
|
|
67
|
+
}
|
|
68
|
+
if (property.name.text === "timeout" && ts.isNumericLiteral(property.initializer)) {
|
|
69
|
+
result.timeout = Number(property.initializer.text);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
found = result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ts.forEachChild(node, inspect);
|
|
77
|
+
};
|
|
78
|
+
inspect(sourceFile);
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Bundles one hook source into a self-contained ESM executable whose entry
|
|
83
|
+
* wrapper calls this agent's `execute`.
|
|
84
|
+
*/
|
|
85
|
+
export async function compileAntigravityHook(sourcePath, loaders, sourcemap = true) {
|
|
86
|
+
const resolveDir = path.dirname(sourcePath);
|
|
87
|
+
const runtimeAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./transport.js"), resolveDir);
|
|
88
|
+
const runtimeRelative = path.relative(resolveDir, runtimeAbsolute).replace(/\\/g, "/");
|
|
89
|
+
const runtimeSpecifier = runtimeRelative.startsWith(".") ? runtimeRelative : `./${runtimeRelative}`;
|
|
90
|
+
const entry = [
|
|
91
|
+
`import hook from ${JSON.stringify(`./${path.basename(sourcePath)}`)};`,
|
|
92
|
+
`import { execute } from ${JSON.stringify(runtimeSpecifier)};`,
|
|
93
|
+
`execute(hook);`,
|
|
94
|
+
``,
|
|
95
|
+
].join("\n");
|
|
96
|
+
const built = await esbuild.build({
|
|
97
|
+
stdin: {
|
|
98
|
+
contents: entry,
|
|
99
|
+
resolveDir,
|
|
100
|
+
sourcefile: `${path.basename(sourcePath, path.extname(sourcePath))}-entry.ts`,
|
|
101
|
+
loader: "ts",
|
|
102
|
+
},
|
|
103
|
+
loader: loaders,
|
|
104
|
+
format: "esm",
|
|
105
|
+
platform: "node",
|
|
106
|
+
target: "node20",
|
|
107
|
+
bundle: true,
|
|
108
|
+
sourcemap: sourcemap ? "inline" : false,
|
|
109
|
+
write: false,
|
|
110
|
+
external: ["node:*", "fs", "path", "os", "crypto", "module", "url"],
|
|
111
|
+
mainFields: ["module", "main"],
|
|
112
|
+
conditions: ["import", "node"],
|
|
113
|
+
preserveSymlinks: true,
|
|
114
|
+
});
|
|
115
|
+
const content = built.outputFiles?.[0]?.text;
|
|
116
|
+
if (content === undefined) {
|
|
117
|
+
throw new Error(`esbuild produced no output for ${sourcePath}`);
|
|
118
|
+
}
|
|
119
|
+
return { content, contentHash: sha256Prefix8(content) };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Converts a source file's basename to the manifest's top-level hook name.
|
|
123
|
+
* Names must be unique across the whole manifest, since the host merges
|
|
124
|
+
* same-named entries; a collision is suffixed rather than silently dropped.
|
|
125
|
+
*/
|
|
126
|
+
function assignHookName(sourcePath, taken) {
|
|
127
|
+
const base = path.basename(sourcePath, path.extname(sourcePath));
|
|
128
|
+
if (!taken.has(base)) {
|
|
129
|
+
taken.add(base);
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
let suffix = 2;
|
|
133
|
+
while (taken.has(`${base}-${suffix}`)) {
|
|
134
|
+
suffix++;
|
|
135
|
+
}
|
|
136
|
+
const unique = `${base}-${suffix}`;
|
|
137
|
+
taken.add(unique);
|
|
138
|
+
return unique;
|
|
139
|
+
}
|
|
140
|
+
async function compileAll(hookFiles, binDir, loaders, options) {
|
|
141
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
142
|
+
const compiled = [];
|
|
143
|
+
const taken = new Set();
|
|
144
|
+
const written = new Set();
|
|
145
|
+
for (const sourcePath of hookFiles) {
|
|
146
|
+
const metadata = analyzeAntigravityHookFile(sourcePath);
|
|
147
|
+
if (metadata === undefined) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const { content, contentHash } = await compileAntigravityHook(sourcePath, loaders, options.sourcemap);
|
|
151
|
+
const hookName = assignHookName(sourcePath, taken);
|
|
152
|
+
const filename = options.stableNames ? `${hookName}.mjs` : `${hookName}.${contentHash}.mjs`;
|
|
153
|
+
const outputPath = path.join(binDir, filename);
|
|
154
|
+
fs.writeFileSync(outputPath, `#!/usr/bin/env -S node --enable-source-maps\n${content}`, {
|
|
155
|
+
encoding: "utf-8",
|
|
156
|
+
mode: 0o755,
|
|
157
|
+
});
|
|
158
|
+
written.add(filename);
|
|
159
|
+
compiled.push({ sourcePath, outputPath, hookName, metadata });
|
|
160
|
+
}
|
|
161
|
+
pruneStaleHashedBundles(binDir, written);
|
|
162
|
+
return compiled;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Renders the command string for one bundle. The host runs each handler
|
|
166
|
+
* through `sh -c` with the working directory set to the directory holding
|
|
167
|
+
* `hooks.json`, so a manifest-relative path is portable across every install
|
|
168
|
+
* location without substitution.
|
|
169
|
+
*/
|
|
170
|
+
function commandFor(manifestDir, outputPath, executable) {
|
|
171
|
+
const relative = path.relative(manifestDir, outputPath).replace(/\\/g, "/");
|
|
172
|
+
const specifier = relative.startsWith(".") ? relative : `./${relative}`;
|
|
173
|
+
return `${executable} ${JSON.stringify(specifier)}`;
|
|
174
|
+
}
|
|
175
|
+
function timeoutSeconds(timeoutMs) {
|
|
176
|
+
if (timeoutMs === undefined) {
|
|
177
|
+
return DEFAULT_TIMEOUT_SECONDS;
|
|
178
|
+
}
|
|
179
|
+
return Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Builds the manifest. Each compiled hook becomes one top-level named entry
|
|
183
|
+
* holding exactly one event, which is what the host merges across plugins.
|
|
184
|
+
*/
|
|
185
|
+
export function generateAntigravityHooksJson(compiled, manifestPath, executable = "node") {
|
|
186
|
+
const manifestDir = path.dirname(manifestPath);
|
|
187
|
+
const manifest = {};
|
|
188
|
+
for (const entry of compiled) {
|
|
189
|
+
const handler = {
|
|
190
|
+
type: "command",
|
|
191
|
+
command: commandFor(manifestDir, entry.outputPath, executable),
|
|
192
|
+
timeout: timeoutSeconds(entry.metadata.timeout),
|
|
193
|
+
};
|
|
194
|
+
const event = entry.metadata.hookEventName;
|
|
195
|
+
const named = {};
|
|
196
|
+
if (GROUPED_EVENTS.has(event)) {
|
|
197
|
+
const group = { matcher: entry.metadata.matcher ?? MATCH_ALL, hooks: [handler] };
|
|
198
|
+
if (event === "PreToolUse") {
|
|
199
|
+
named.PreToolUse = [group];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
named.PostToolUse = [group];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (event === "PreInvocation") {
|
|
206
|
+
named.PreInvocation = [handler];
|
|
207
|
+
}
|
|
208
|
+
else if (event === "PostInvocation") {
|
|
209
|
+
named.PostInvocation = [handler];
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
named.Stop = [handler];
|
|
213
|
+
}
|
|
214
|
+
manifest[entry.hookName] = named;
|
|
215
|
+
}
|
|
216
|
+
return manifest;
|
|
217
|
+
}
|
|
218
|
+
/** Validates the Antigravity build arguments beyond the shared `--agent` gate. */
|
|
219
|
+
export function validateAntigravityArgs(args) {
|
|
220
|
+
if (args.scaffold !== undefined && args.scaffold !== "") {
|
|
221
|
+
if (args.output === "") {
|
|
222
|
+
return "Scaffold mode requires -o/--output";
|
|
223
|
+
}
|
|
224
|
+
if (args.hooks === undefined || args.hooks === "") {
|
|
225
|
+
return "Scaffold mode requires --hooks";
|
|
226
|
+
}
|
|
227
|
+
const unknown = args.hooks
|
|
228
|
+
.split(",")
|
|
229
|
+
.map((value) => value.trim())
|
|
230
|
+
.filter((value) => value.length > 0)
|
|
231
|
+
.filter((value) => !HOOK_EVENT_NAMES.some((name) => name.toLowerCase() === value.toLowerCase()));
|
|
232
|
+
if (unknown.length > 0) {
|
|
233
|
+
return `Unknown Antigravity hook name(s): ${unknown.join(", ")}. Valid names: ${HOOK_EVENT_NAMES.join(", ")}`;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (args.input === "") {
|
|
238
|
+
return "Missing required argument: -i/--input";
|
|
239
|
+
}
|
|
240
|
+
if (args.output === "") {
|
|
241
|
+
return "Missing required argument: -o/--output";
|
|
242
|
+
}
|
|
243
|
+
for (const flag of args.loaderFlags) {
|
|
244
|
+
if (parseEsbuildLoaderFlag(flag) === undefined) {
|
|
245
|
+
return `Invalid --loader value: ${flag}. Expected .ext=type`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Runs the Antigravity build or scaffold branch. Bundle filenames are stable
|
|
252
|
+
* by default: a manifest committed into a plugin tree must not churn on every
|
|
253
|
+
* rebuild, and the host reads the command string rather than globbing the
|
|
254
|
+
* directory.
|
|
255
|
+
*/
|
|
256
|
+
export async function runAntigravityCli(args) {
|
|
257
|
+
if (args.scaffold !== undefined && args.scaffold !== "") {
|
|
258
|
+
const { scaffoldProject } = await import("./scaffold.js");
|
|
259
|
+
scaffoldProject({
|
|
260
|
+
directory: path.resolve(process.cwd(), args.scaffold),
|
|
261
|
+
hooks: (args.hooks ?? "")
|
|
262
|
+
.split(",")
|
|
263
|
+
.map((value) => value.trim())
|
|
264
|
+
.filter((value) => value.length > 0),
|
|
265
|
+
outputPath: args.output,
|
|
266
|
+
});
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
const cwd = process.cwd();
|
|
270
|
+
const manifestPath = path.resolve(cwd, args.output);
|
|
271
|
+
const binDir = path.join(path.dirname(manifestPath), "bin");
|
|
272
|
+
const hookFiles = await discoverHookSourceFiles(args.input, cwd);
|
|
273
|
+
const loaders = buildEsbuildLoaderMap(args.loaderFlags);
|
|
274
|
+
const compiled = await compileAll(hookFiles, binDir, loaders, {
|
|
275
|
+
stableNames: args.stableNames ?? true,
|
|
276
|
+
sourcemap: args.sourcemap ?? true,
|
|
277
|
+
});
|
|
278
|
+
const manifest = generateAntigravityHooksJson(compiled, manifestPath, args.executable ?? "node");
|
|
279
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
280
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
281
|
+
return compiled.length;
|
|
282
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const PACKAGE_NAME = "@goodfoot/agent-hooks";
|
|
2
|
+
/** The host's own default handler timeout, in seconds (`CONTRACT.md`, Hook Handler Fields). */
|
|
3
|
+
export const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
4
|
+
export const DEFAULT_ESBUILD_LOADERS = {
|
|
5
|
+
".md": "text",
|
|
6
|
+
};
|
|
7
|
+
/** Maps each exported factory name to the event it binds. */
|
|
8
|
+
export const HOOK_FACTORY_TO_EVENT = {
|
|
9
|
+
preToolUseHook: "PreToolUse",
|
|
10
|
+
postToolUseHook: "PostToolUse",
|
|
11
|
+
preInvocationHook: "PreInvocation",
|
|
12
|
+
postInvocationHook: "PostInvocation",
|
|
13
|
+
stopHook: "Stop",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Events whose `hooks.json` entry is a grouped `{ matcher, hooks }` wrapper.
|
|
17
|
+
* The remaining three are flat lists of handler objects; the host ignores a
|
|
18
|
+
* matcher on them.
|
|
19
|
+
*/
|
|
20
|
+
export const GROUPED_EVENTS = new Set(["PreToolUse", "PostToolUse"]);
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Antigravity hook event names
|
|
2
|
+
* Antigravity hook event names, the grouped/flat structural split, and the
|
|
3
|
+
* (empty) advisory allow-list.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* "continue"`.
|
|
5
|
+
* The host's hook reference — pinned in this directory's `CONTRACT.md` —
|
|
6
|
+
* names five events and splits them structurally: `PreToolUse` and
|
|
7
|
+
* `PostToolUse` wrap their handlers in a `{ matcher, hooks }` group, while
|
|
8
|
+
* `PreInvocation`, `PostInvocation`, and `Stop` list handler objects
|
|
9
|
+
* directly and ignore any matcher. {@link EVENTS_WITH_MATCHER} is that split,
|
|
10
|
+
* and `cli-support.ts` emits `hooks.json` from it.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
12
|
+
* The advisory allow-list is empty, and stays empty: the reference names no
|
|
13
|
+
* event as safe to fail open. That default is load-bearing here in a way it
|
|
14
|
+
* is not on the other two agents, because Antigravity has no exit-code
|
|
15
|
+
* channel at all (every event is payload-only — see `transport.ts` and
|
|
16
|
+
* `outputs.ts`'s {@link AntigravityBlockError}), so a fail-open swallow has
|
|
17
|
+
* no loud path to degrade to. `AntigravityBlockError` is what would make
|
|
18
|
+
* widening the list safe at all: an intentional block computed mid-throw
|
|
19
|
+
* still survives `unexpectedError: "continue"` via `drive()`'s pre-policy
|
|
20
|
+
* classification, exactly as it does for the other two agents.
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
/** All Antigravity hook event names as a readonly array. */
|
|
@@ -28,15 +28,17 @@ export const HOOK_EVENT_NAMES = [
|
|
|
28
28
|
"PostInvocation",
|
|
29
29
|
"Stop",
|
|
30
30
|
];
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The two events whose `hooks.json` entry is grouped under a `matcher`. The
|
|
33
|
+
* other three are flat: the host ignores a matcher on them entirely.
|
|
34
|
+
*/
|
|
32
35
|
export const EVENTS_WITH_MATCHER = ["PreToolUse", "PostToolUse"];
|
|
33
36
|
/**
|
|
34
37
|
* Events never allowed the fail-open `unexpectedError: "continue"` policy.
|
|
35
38
|
*
|
|
36
|
-
* Every shipped event
|
|
37
|
-
*
|
|
38
|
-
* justifies an entry"), not a silent gap.
|
|
39
|
+
* Every shipped event. The host's hook reference names no event as safe to
|
|
40
|
+
* fail open, so this is a transcribed conclusion, not a placeholder.
|
|
39
41
|
*/
|
|
40
42
|
export const EXCLUDED_FROM_ADVISORY = [...HOOK_EVENT_NAMES];
|
|
41
|
-
/** The advisory allow-list. Empty
|
|
43
|
+
/** The advisory allow-list. Empty: `CONTRACT.md` names no event safe to fail open. */
|
|
42
44
|
export const ADVISORY_EVENTS = HOOK_EVENT_NAMES.filter((eventName) => !EXCLUDED_FROM_ADVISORY.includes(eventName));
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
* Typed factory functions for Antigravity hooks, built on the shared core
|
|
3
3
|
* {@link ../../core/define-hook.ts|defineHook}.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* release); {@link HookConfigFor} additionally narrows the policy field at
|
|
11
|
-
* compile time.
|
|
5
|
+
* Every factory injects the advisory allow-list policy gate, so
|
|
6
|
+
* `unexpectedError: "continue"` fails closed at factory-call time for every
|
|
7
|
+
* event ({@link ADVISORY_EVENTS} is empty, and `CONTRACT.md` names no event
|
|
8
|
+
* safe to fail open); {@link HookConfigFor} additionally narrows the policy
|
|
9
|
+
* field at compile time.
|
|
12
10
|
*
|
|
13
11
|
* Antigravity's config surface is a single conditional type rather than the
|
|
14
12
|
* separate matcher/no-matcher interface pair Claude Code and Codex use: only
|
|
15
13
|
* two of the five events (`PreToolUse`, `PostToolUse`) carry a `matcher`
|
|
16
14
|
* ({@link EVENTS_WITH_MATCHER} in `events.ts`), and a conditional type on the
|
|
17
15
|
* bound event expresses that distinction without duplicating the base field
|
|
18
|
-
* list twice.
|
|
16
|
+
* list twice. The host ignores a matcher on the other three events entirely,
|
|
17
|
+
* so accepting one there would be a silent no-op.
|
|
19
18
|
* @module
|
|
20
19
|
*/
|
|
21
20
|
import { defineHook } from "../../core/define-hook.js";
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@goodfoot/agent-hooks/antigravity` — the Antigravity agent surface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
* builders, and a conformance matrix only. `packages/agent-hooks/package.json`
|
|
8
|
-
* deliberately does not add a `./antigravity` `exports` entry this release —
|
|
9
|
-
* see `tests/export-surface.test.ts`, which asserts the exports map stays
|
|
10
|
-
* closed at `.`/`./claude-code`/`./codex`. This module exists for in-repo
|
|
11
|
-
* testing (the conformance matrix imports it directly by relative path) and
|
|
12
|
-
* for the follow-up card that resolves the CLI-availability question to
|
|
13
|
-
* build on without re-deriving the surface from scratch.
|
|
4
|
+
* Re-exports the five hook factories, their per-event output builders, the
|
|
5
|
+
* `AntigravityBlockError` block signal, the wire input/output types, and the
|
|
6
|
+
* runtime `execute` entry point. No default export.
|
|
14
7
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* Every shape here is transcribed from `CONTRACT.md` in this directory, which
|
|
9
|
+
* pins the host's own hook reference. Two consequences shape the whole
|
|
10
|
+
* surface: keys are camelCase, and each event has a **different** stdout
|
|
11
|
+
* shape rather than one shared envelope.
|
|
19
12
|
* @module
|
|
20
13
|
*/
|
|
21
|
-
// Advisory allow-list events
|
|
22
14
|
export { ADVISORY_EVENTS, EVENTS_WITH_MATCHER, EXCLUDED_FROM_ADVISORY, HOOK_EVENT_NAMES } from "./events.js";
|
|
23
|
-
// Hook factory functions - all 5 event types
|
|
24
15
|
export { postInvocationHook, postToolUseHook, preInvocationHook, preToolUseHook, stopHook } from "./hooks.js";
|
|
25
|
-
// Output types and builders (AntigravityBlockError subclasses core HookBlockError)
|
|
26
16
|
export { AntigravityBlockError, EXIT_CODES, postInvocationOutput, postToolUseOutput, preInvocationOutput, preToolUseOutput, stopOutput, } from "./outputs.js";
|
|
27
|
-
// Runtime exports - execute function and transport factory
|
|
28
17
|
export { createAntigravityTransport, execute } from "./transport.js";
|