-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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 all commits
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,43 @@ | ||
| { | ||
| "agentmemory": { | ||
| "enabled": true, | ||
| "PreInvocation": [ | ||
| { | ||
| "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": [ | ||
| { | ||
| "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,114 @@ | ||
| #!/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; | ||
| } | ||
| 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; | ||
| } | ||
| 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 []; | ||
| } | ||
| } | ||
| function responseFor(event) { | ||
| return event === "PreToolUse" ? "{\"decision\":\"allow\"}" : "{}"; | ||
| } | ||
| 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(responseFor(process.argv[2] ?? "")); | ||
| process.exit(0); | ||
| }); | ||
| //#endregion | ||
| export { normalizePayload, responseFor, 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,97 @@ | ||
| 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, | ||
| containsSpaces, | ||
| type AntigravityHookManifest, | ||
| } from "./antigravity-hooks.js"; | ||
| import { findPluginRoot } from "./codex-hooks.js"; | ||
| import { | ||
| backupFile, | ||
| logBackup, | ||
| logInstalled, | ||
| readJsonSafe, | ||
| writeJsonAtomic, | ||
| } from "./util.js"; | ||
|
|
||
| // The `agy` CLI shares no configuration with the Antigravity IDE that | ||
| // `antigravity.ts` wires — it reads MCP from ~/.gemini/config/mcp_config.json | ||
| // and hooks from ~/.gemini/config/hooks.json (per-workspace overrides in | ||
| // <repo>/.agents/hooks.json). Detection keys off ~/.gemini/antigravity-cli/, | ||
| // which only the CLI creates; ~/.gemini/ alone would also match Gemini CLI. | ||
| // 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`, replacing only the bundle agentmemory owns. | ||
| */ | ||
| function installAntigravityCliHooks(opts: ConnectOptions): ConnectResult { | ||
| let pluginRoot: string; | ||
| try { | ||
| pluginRoot = findPluginRoot(); | ||
| } catch (err) { | ||
| return { | ||
| kind: "skipped", | ||
| reason: err instanceof Error ? err.message : String(err), | ||
| }; | ||
| } | ||
|
|
||
| // agy honours no quoting, so a space in the path yields hooks that load | ||
| // but never run. Refuse rather than install something that can only fail. | ||
| if (containsSpaces(pluginRoot)) { | ||
| return { | ||
| kind: "skipped", | ||
| reason: `Antigravity CLI cannot run hook commands whose path contains spaces, and agentmemory is installed at ${pluginRoot}. Reinstall it under a space-free path to use --with-hooks; MCP works either way.`, | ||
| }; | ||
| } | ||
|
|
||
| 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.