diff options
| author | Ludovico Magnocavallo <ludo@qix.it> | 2026-08-01 18:32:47 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 19:32:47 +0300 |
| commit | 679584fdd49f9dee5ad95755af65eedc7d0f455c (patch) | |
| tree | b21359553af294c2a2d57726e99029d86534cda0 /src/integration/targets.rs | |
| parent | 219e0be351436b5bf1ccf5b7c13dbf6c2cdb0986 (diff) | |
feat: add antigravity-cli integration (#2087)
* feat: add antigravity_cli integration harness
Register the `antigravity_cli` target, resolve its configuration
directory to `~/.gemini/antigravity-cli/`, wire target actions,
and add lifecycle hook scripts for Unix and Windows.
* feat: support session resume for antigravity-cli
* fix: correct antigravity-cli hook config path, shape, and agent label
Antigravity CLI reads global customizations from ~/.gemini/config, keys
hooks.json by hook name, and only accepts the matcher/hooks wrapper for
the tool events. The previous install wrote event arrays at the top level
of ~/.gemini/antigravity-cli/hooks.json, which agy never reads and would
reject anyway, so no Herdr hook ever ran.
Session reports were dropped for a third reason: the hooks reported the
agent as antigravity-cli, but the server normalizes that to the canonical
label agy before matching, so is_official_agent_source never matched and
agy --conversation resume was unreachable.
- Resolve the config dir to ~/.gemini/config
- Nest Herdr entries under a Herdr-owned "herdr" block that install
rewrites and uninstall removes, leaving other named hooks untouched
- Emit grouped entries for PreToolUse/PostToolUse and flat handler lists
for PreInvocation/PostInvocation/Stop
- Report the canonical agy label from both hook assets and match it in
agent_resume
- Compile-gate the hook asset constants and stop shadowing $args in the
PowerShell hook
- Document the real directory, that it must already exist, and the
named-block behavior
refs #1011
refs #1571
* fix: make antigravity-cli integration session-only
antigravity cli cannot express lifecycle safely: there is no blocked
event, postinvocation is skipped on interruption, and stop fires at the
end of a turn rather than on process exit, which left stale state and
released authority at the wrong time.
report only the conversation on preinvocation and let screen detection
own agent state. resume via `agy --conversation <id>` is unchanged.
* fix: update antigravity session after conversation switch
---------
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/integration/targets.rs')
| -rw-r--r-- | src/integration/targets.rs | 141 |
1 files changed, 121 insertions, 20 deletions
diff --git a/src/integration/targets.rs b/src/integration/targets.rs index 87000eae..575c7e46 100644 --- a/src/integration/targets.rs +++ b/src/integration/targets.rs @@ -2,7 +2,7 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use super::command::{hook_command, shell_single_quote}; use super::config_edit::{ @@ -13,32 +13,33 @@ use super::config_edit::{ remove_hook_commands, remove_kimi_config_block, remove_simple_command_hook, }; use super::env::{ - claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir, grok_dir, hermes_dir, - hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir, opencode_dir, - pi_extension_dir, qodercli_dir, + antigravity_cli_dir, claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir, + grok_dir, hermes_dir, hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir, + opencode_dir, pi_extension_dir, qodercli_dir, }; use super::file_ops::{ make_executable, remove_dir_all_if_exists, remove_file_if_exists, remove_legacy_bash_hook_file, }; use super::types::{ - ClaudeInstallPaths, ClaudeUninstallResult, CodexInstallPaths, CodexUninstallResult, - CopilotInstallPaths, CopilotUninstallResult, CursorInstallPaths, CursorUninstallResult, - DevinInstallPaths, DevinUninstallResult, DroidInstallPaths, DroidUninstallResult, - GrokInstallPaths, GrokUninstallResult, HermesInstallPaths, HermesUninstallResult, - KiloInstallPaths, KiloUninstallResult, KimiInstallPaths, KimiUninstallResult, - MastracodeInstallPaths, MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult, - OpenCodeInstallPaths, OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths, - QodercliUninstallResult, + AntigravityCliInstallPaths, AntigravityCliUninstallResult, ClaudeInstallPaths, + ClaudeUninstallResult, CodexInstallPaths, CodexUninstallResult, CopilotInstallPaths, + CopilotUninstallResult, CursorInstallPaths, CursorUninstallResult, DevinInstallPaths, + DevinUninstallResult, DroidInstallPaths, DroidUninstallResult, GrokInstallPaths, + GrokUninstallResult, HermesInstallPaths, HermesUninstallResult, KiloInstallPaths, + KiloUninstallResult, KimiInstallPaths, KimiUninstallResult, MastracodeInstallPaths, + MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult, OpenCodeInstallPaths, + OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths, QodercliUninstallResult, }; use super::{ - CLAUDE_HOOK_ASSET, CLAUDE_HOOK_INSTALL_NAME, CODEX_HOOK_ASSET, CODEX_HOOK_INSTALL_NAME, - COPILOT_HOOK_ASSET, COPILOT_HOOK_EVENTS, COPILOT_HOOK_INSTALL_NAME, - COPILOT_REMOVED_LIFECYCLE_HOOK_EVENTS, CURSOR_HOOK_ASSET, CURSOR_HOOK_INSTALL_NAME, - DEVIN_HOOK_ASSET, DEVIN_HOOK_EVENTS, DEVIN_HOOK_INSTALL_NAME, - DEVIN_REMOVED_LIFECYCLE_HOOK_EVENTS, DROID_HOOK_ASSET, DROID_HOOK_EVENTS, - DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS, GROK_HOOK_ASSET, - GROK_HOOK_CONFIG_INSTALL_NAME, GROK_HOOK_INSTALL_NAME, HERMES_PLUGIN_INIT_ASSET, - HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_MANIFEST_ASSET, + ANTIGRAVITY_CLI_HOOK_ASSET, ANTIGRAVITY_CLI_HOOK_BLOCK_NAME, ANTIGRAVITY_CLI_HOOK_EVENTS, + ANTIGRAVITY_CLI_HOOK_INSTALL_NAME, ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC, CLAUDE_HOOK_ASSET, + CLAUDE_HOOK_INSTALL_NAME, CODEX_HOOK_ASSET, CODEX_HOOK_INSTALL_NAME, COPILOT_HOOK_ASSET, + COPILOT_HOOK_EVENTS, COPILOT_HOOK_INSTALL_NAME, COPILOT_REMOVED_LIFECYCLE_HOOK_EVENTS, + CURSOR_HOOK_ASSET, CURSOR_HOOK_INSTALL_NAME, DEVIN_HOOK_ASSET, DEVIN_HOOK_EVENTS, + DEVIN_HOOK_INSTALL_NAME, DEVIN_REMOVED_LIFECYCLE_HOOK_EVENTS, DROID_HOOK_ASSET, + DROID_HOOK_EVENTS, DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS, + GROK_HOOK_ASSET, GROK_HOOK_CONFIG_INSTALL_NAME, GROK_HOOK_INSTALL_NAME, + HERMES_PLUGIN_INIT_ASSET, HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_MANIFEST_ASSET, HERMES_PLUGIN_MANIFEST_INSTALL_NAME, KILO_PLUGIN_ASSET, KILO_PLUGIN_INSTALL_NAME, KIMI_HOOK_ASSET, KIMI_HOOK_INSTALL_NAME, MASTRACODE_HOOK_ASSET, MASTRACODE_HOOK_EVENTS, MASTRACODE_HOOK_INSTALL_NAME, MASTRACODE_HOOK_TIMEOUT_MS, MASTRACODE_REMOVED_HOOK_EVENTS, @@ -1203,6 +1204,106 @@ pub(crate) fn uninstall_mastracode() -> io::Result<MastracodeUninstallResult> { }) } +pub(crate) fn install_antigravity_cli() -> io::Result<AntigravityCliInstallPaths> { + let dir = antigravity_cli_dir()?; + if !dir.is_dir() { + return Err(io::Error::other(format!( + "antigravity cli config directory not found at {}. install antigravity cli first", + dir.display() + ))); + } + + let hooks_dir = dir.join("hooks"); + fs::create_dir_all(&hooks_dir)?; + + let hook_path = hooks_dir.join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME); + fs::write(&hook_path, ANTIGRAVITY_CLI_HOOK_ASSET)?; + make_executable(&hook_path)?; + + let hooks_path = dir.join("hooks.json"); + let mut hooks_file = if hooks_path.is_file() { + serde_json::from_str::<Value>(&fs::read_to_string(&hooks_path)?).map_err(|err| { + io::Error::other(format!("failed to parse {}: {err}", hooks_path.display())) + })? + } else { + json!({}) + }; + + let hooks = hooks_file.as_object_mut().ok_or_else(|| { + io::Error::other(format!( + "antigravity cli hooks file at {} must be a JSON object", + hooks_path.display() + )) + })?; + + // The Herdr block is Herdr-owned, so rewrite it wholesale and leave every + // other named hook untouched. + hooks.insert( + ANTIGRAVITY_CLI_HOOK_BLOCK_NAME.to_string(), + antigravity_cli_hook_block(&hook_path), + ); + + fs::write(&hooks_path, serde_json::to_string_pretty(&hooks_file)?)?; + + Ok(AntigravityCliInstallPaths { + hook_path, + hooks_path, + }) +} + +/// Builds the Herdr-owned `hooks.json` block for Antigravity CLI. +/// +/// Every event Herdr registers takes a flat handler list; the `matcher`/`hooks` +/// group is only valid for the tool events, which Herdr does not use. +fn antigravity_cli_hook_block(hook_path: &Path) -> Value { + let mut block = Map::new(); + for (event, action) in ANTIGRAVITY_CLI_HOOK_EVENTS { + let handler = json!({ + "type": "command", + "command": hook_command(hook_path, Some(action)), + "timeout": ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC, + }); + block.insert(event.to_string(), json!([handler])); + } + Value::Object(block) +} + +pub(crate) fn uninstall_antigravity_cli() -> io::Result<AntigravityCliUninstallResult> { + let dir = antigravity_cli_dir()?; + let hook_path = dir.join("hooks").join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME); + let hooks_path = dir.join("hooks.json"); + let mut updated_hooks = false; + + if hooks_path.is_file() { + let mut hooks_file = serde_json::from_str::<Value>(&fs::read_to_string(&hooks_path)?) + .map_err(|err| { + io::Error::other(format!("failed to parse {}: {err}", hooks_path.display())) + })?; + + let hooks = hooks_file.as_object_mut().ok_or_else(|| { + io::Error::other(format!( + "antigravity cli hooks file at {} must be a JSON object", + hooks_path.display() + )) + })?; + + updated_hooks = hooks.remove(ANTIGRAVITY_CLI_HOOK_BLOCK_NAME).is_some(); + + if updated_hooks { + fs::write(&hooks_path, serde_json::to_string_pretty(&hooks_file)?)?; + } + } + + let removed_hook_file = remove_file_if_exists(&hook_path)?; + + Ok(AntigravityCliUninstallResult { + hook_path, + hooks_path, + removed_hook_file, + updated_hooks, + }) +} + /// The complete Herdr-owned Grok hook config. Installation and status share /// this value so any config drift is reported as outdated. pub(crate) fn grok_hook_config(hook_path: &Path) -> Value { |
