-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(cli): native hooks adapter for Antigravity CLI (agy) #1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rohitg00
merged 5 commits into
rohitg00:main
from
berthojoris:feature/antigravity-native-hooks
Aug 3, 2026
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1b43e89
feat(cli): native hooks adapter for Antigravity CLI (agy)
berthojoris bb74810
fix(cli): keep $-bearing plugin paths literal when resolving hook com…
berthojoris 278fb05
fix(antigravity): emit an explicit allow decision from the PreToolUse…
berthojoris 6e481cb
fix(antigravity): match agy's real hooks.json schema, verified agains…
berthojoris 3a095ba
refactor(antigravity): cut comment volume to match the sibling adapters
berthojoris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| { | ||
| "agentmemory": { | ||
| "enabled": true, | ||
| "PreInvocation": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/antigravity-bridge.mjs\" PreInvocation", | ||
| "timeout": 10 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "view_file|view_code_item|read_file|edit_file|replace_file_content|write_to_file|create_file|grep_search|codebase_search|find_by_name|list_dir", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/antigravity-bridge.mjs\" PreToolUse", | ||
| "timeout": 10 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PostToolUse": [ | ||
| { | ||
| "matcher": "", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/antigravity-bridge.mjs\" PostToolUse", | ||
| "timeout": 10 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/antigravity-bridge.mjs\" Stop", | ||
| "timeout": 10 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| #!/usr/bin/env node | ||
| import { spawnSync } from "node:child_process"; | ||
| import { dirname, join, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| //#region src/hooks/antigravity-bridge.ts | ||
| const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url)); | ||
| const TOOL_NAME_MAP = { | ||
| view_file: "read", | ||
| view_line_range: "read", | ||
| view_code_item: "read", | ||
| read_file: "read", | ||
| read_url_content: "read", | ||
| edit_file: "edit", | ||
| replace_file_content: "edit", | ||
| propose_code: "edit", | ||
| write_to_file: "write", | ||
| create_file: "write", | ||
| grep_search: "grep", | ||
| codebase_search: "grep", | ||
| find_by_name: "glob", | ||
| list_dir: "glob" | ||
| }; | ||
| const ARG_KEY_MAP = { | ||
| AbsolutePath: "file_path", | ||
| TargetFile: "file_path", | ||
| DirectoryPath: "path", | ||
| SearchDirectory: "path", | ||
| Pattern: "pattern", | ||
| Query: "pattern", | ||
| CommandLine: "command" | ||
| }; | ||
| function asObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0; | ||
| } | ||
| function firstString(...values) { | ||
| for (const v of values) if (typeof v === "string" && v.length > 0) return v; | ||
| } | ||
| function normalizeToolArgs(args) { | ||
| if (!args) return {}; | ||
| const out = { ...args }; | ||
| for (const [from, to] of Object.entries(ARG_KEY_MAP)) if (out[to] === void 0 && args[from] !== void 0) out[to] = args[from]; | ||
| return out; | ||
| } | ||
| /** | ||
| * Translate one Antigravity hook payload into the flat, snake_case shape | ||
| * the bundled hooks consume. Unknown fields are passed through so future | ||
| * Antigravity additions stay visible to the capture pipeline. | ||
| */ | ||
| function normalizePayload(event, raw) { | ||
| const toolCall = asObject(raw["toolCall"]); | ||
| const workspacePaths = Array.isArray(raw["workspacePaths"]) ? raw["workspacePaths"] : []; | ||
| const sessionId = firstString(raw["conversationId"], raw["session_id"], raw["sessionId"]) ?? "unknown"; | ||
| const cwd = firstString(raw["cwd"], workspacePaths[0]) ?? process.cwd(); | ||
| const out = { | ||
| ...raw, | ||
| session_id: sessionId, | ||
| cwd, | ||
| hook_event_name: event | ||
| }; | ||
| const transcriptPath = firstString(raw["transcript_path"], raw["transcriptPath"]); | ||
| if (transcriptPath) out["transcript_path"] = transcriptPath; | ||
| if (toolCall) { | ||
| const args = normalizeToolArgs(asObject(toolCall["args"]) ?? asObject(toolCall["toolArgs"])); | ||
| const rawName = firstString(toolCall["name"], toolCall["toolName"], args["ToolName"], args["toolName"]); | ||
| if (rawName) { | ||
| out["tool_name"] = TOOL_NAME_MAP[rawName] ?? rawName; | ||
| out["native_tool_name"] = rawName; | ||
| } | ||
| out["tool_input"] = args; | ||
| const result = toolCall["result"] ?? raw["toolResult"] ?? raw["result"]; | ||
| if (result !== void 0) out["tool_result"] = result; | ||
| } | ||
| return out; | ||
| } | ||
| /** | ||
| * Map an Antigravity event to the bundled scripts it should drive. | ||
| * | ||
| * PreInvocation stands in for both SessionStart and UserPromptSubmit: the | ||
| * first invocation of a conversation opens the session, every later one is | ||
| * a fresh user turn. PostInvocation is deliberately unmapped — PostToolUse | ||
| * already captures the work, and firing again would double-record it. | ||
| */ | ||
| function targetsFor(event, raw) { | ||
| switch (event) { | ||
| case "PreInvocation": { | ||
| const n = raw["invocationNum"]; | ||
| return typeof n !== "number" || n <= 1 ? ["session-start.mjs", "prompt-submit.mjs"] : ["prompt-submit.mjs"]; | ||
| } | ||
| case "PreToolUse": return ["pre-tool-use.mjs"]; | ||
| case "PostToolUse": return ["post-tool-use.mjs"]; | ||
| case "Stop": return ["stop.mjs", "session-end.mjs"]; | ||
| default: return []; | ||
| } | ||
| } | ||
| async function main() { | ||
| const event = process.argv[2]; | ||
| if (!event) return; | ||
| let input = ""; | ||
| for await (const chunk of process.stdin) input += chunk; | ||
| let raw; | ||
| try { | ||
| raw = JSON.parse(input); | ||
| } catch { | ||
| return; | ||
| } | ||
| if (!raw || typeof raw !== "object") return; | ||
| const payload = JSON.stringify(normalizePayload(event, raw)); | ||
| for (const script of targetsFor(event, raw)) spawnSync(process.execPath, [join(SCRIPTS_DIR, script)], { | ||
| input: payload, | ||
| stdio: [ | ||
| "pipe", | ||
| "ignore", | ||
| "ignore" | ||
| ] | ||
| }); | ||
| } | ||
| if (process.argv[1] !== void 0 && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(() => {}).finally(() => { | ||
| process.stdout.write("{}"); | ||
| process.exit(0); | ||
| }); | ||
| //#endregion | ||
| export { normalizePayload, targetsFor }; | ||
|
|
||
| //# sourceMappingURL=antigravity-bridge.mjs.map |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { existsSync, mkdirSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import * as p from "@clack/prompts"; | ||
| import { createJsonMcpAdapter } from "./json-mcp-adapter.js"; | ||
| import type { ConnectOptions, ConnectResult } from "./types.js"; | ||
| import { | ||
| buildMergedAntigravityHooks, | ||
| type AntigravityHookManifest, | ||
| } from "./antigravity-hooks.js"; | ||
| import { findPluginRoot } from "./codex-hooks.js"; | ||
| import { | ||
| backupFile, | ||
| logBackup, | ||
| logInstalled, | ||
| readJsonSafe, | ||
| writeJsonAtomic, | ||
| } from "./util.js"; | ||
|
|
||
| // Antigravity ships two products that do NOT share configuration: | ||
| // | ||
| // • the IDE, wired by `antigravity.ts` via the app-support directory | ||
| // (~/Library/Application Support/Antigravity/User/mcp_config.json); | ||
| // • the `agy` CLI, wired here, which reads its customizations out of | ||
| // ~/.gemini/ — MCP servers from ~/.gemini/config/mcp_config.json and | ||
| // hooks from ~/.gemini/config/hooks.json, with per-workspace overrides | ||
| // in <repo>/.agents/hooks.json. | ||
| // | ||
| // Detection keys off ~/.gemini/antigravity-cli/, which only the CLI | ||
| // creates — ~/.gemini/ alone would also match a Gemini CLI install. | ||
| // | ||
| // Unlike Claude Code, Codex and Droid, Antigravity's hooks.json is a map of | ||
| // *named* hook bundles and exposes only five events, so `--with-hooks` | ||
| // uses the dedicated merge engine in antigravity-hooks.ts and routes every | ||
| // event through the bridge in plugin/scripts/antigravity-bridge.mjs. | ||
| // Sources: antigravity.google/docs/hooks, antigravity.google/docs/cli/using | ||
| const GEMINI_DIR = join(homedir(), ".gemini"); | ||
| const ANTIGRAVITY_CLI_DIR = join(GEMINI_DIR, "antigravity-cli"); | ||
| const CUSTOMIZATION_DIR = join(GEMINI_DIR, "config"); | ||
| const ANTIGRAVITY_CLI_HOOKS = join(CUSTOMIZATION_DIR, "hooks.json"); | ||
|
|
||
| export const adapter = createJsonMcpAdapter({ | ||
| name: "antigravity-cli", | ||
| displayName: "Antigravity CLI (agy)", | ||
| detectDir: ANTIGRAVITY_CLI_DIR, | ||
| configPath: join(CUSTOMIZATION_DIR, "mcp_config.json"), | ||
| docs: "https://github.com/rohitg00/agentmemory#other-agents", | ||
| protocolNote: | ||
| "→ Using MCP via ~/.gemini/config/mcp_config.json (the agy CLI, not the Antigravity IDE — that one is `connect antigravity`). The `/mcp` slash command inside agy lists configured servers. Pass --with-hooks to also install the native ~/.gemini/config/hooks.json auto-capture hooks.", | ||
| installHooks: installAntigravityCliHooks, | ||
| }); | ||
|
|
||
| /** | ||
| * Merge the bundled `plugin/hooks/hooks.antigravity.json` into | ||
| * `~/.gemini/config/hooks.json`. Idempotent in the same way as the Codex | ||
| * and Droid installers: re-running replaces only the hook bundle | ||
| * agentmemory owns and leaves user-authored bundles alone. | ||
| */ | ||
| function installAntigravityCliHooks(opts: ConnectOptions): ConnectResult { | ||
| let pluginRoot: string; | ||
| try { | ||
| pluginRoot = findPluginRoot(); | ||
| } catch (err) { | ||
| return { | ||
| kind: "skipped", | ||
| reason: err instanceof Error ? err.message : String(err), | ||
| }; | ||
| } | ||
|
|
||
| const existing = readJsonSafe<AntigravityHookManifest>(ANTIGRAVITY_CLI_HOOKS); | ||
| const merged = buildMergedAntigravityHooks(existing, pluginRoot); | ||
|
|
||
| if (opts.dryRun) { | ||
| p.log.info( | ||
| `[dry-run] Would ${existing ? "merge" : "create"} ${ANTIGRAVITY_CLI_HOOKS} with ${Object.keys(merged).length} hook bundle(s)`, | ||
| ); | ||
| return { kind: "installed", mutatedPath: ANTIGRAVITY_CLI_HOOKS }; | ||
| } | ||
|
|
||
| let backupPath: string | undefined; | ||
| if (existsSync(ANTIGRAVITY_CLI_HOOKS)) { | ||
| backupPath = backupFile(ANTIGRAVITY_CLI_HOOKS, "antigravity-cli-hooks", "json"); | ||
| logBackup(backupPath); | ||
| } else { | ||
| mkdirSync(CUSTOMIZATION_DIR, { recursive: true }); | ||
| } | ||
|
|
||
| writeJsonAtomic(ANTIGRAVITY_CLI_HOOKS, merged); | ||
|
|
||
| logInstalled("Antigravity CLI hooks", ANTIGRAVITY_CLI_HOOKS); | ||
| p.log.info( | ||
| "User-scope hooks reference absolute paths under the bundled plugin/ dir. Re-run `agentmemory connect antigravity-cli --with-hooks` after upgrading agentmemory to refresh them.", | ||
| ); | ||
|
|
||
| return { | ||
| kind: "installed", | ||
| mutatedPath: ANTIGRAVITY_CLI_HOOKS, | ||
| ...(backupPath !== undefined && { backupPath }), | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lot of similar comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trimmed in
3a095ba. The file header and the per-function block were stating the same things twice — kept one statement of each and dropped the rest. Comment lines:antigravity-cli.ts26 → 12,antigravity-hooks.ts65 → 46,antigravity-bridge.ts70 → 41, which puts them in line withdroid.ts.What I deliberately kept is the agy behaviour I verified against a live 1.0.15 — the two event shapes, the no-quoting rule, the PreToolUse decision contract. That part isn't derivable from the code, and each of those was a real defect here, so a future reader changing the manifest needs the reason. Happy to cut further if you'd rather that lived only in the PR description.