diff --git a/README.md b/README.md index 45acac1c..03433a03 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ | Oh My Pi / OMP | Supported | `pi` / `omp` aliases for MCP config + TypeScript extension. | | Claude Desktop | MCP-only | Uses `mcp-remote`; no lifecycle hooks. | | OpenClaw | Supported | MCP config + native plugin lifecycle hooks. | +| Antigravity CLI | Supported | MCP config (`serverUrl`) + lifecycle hooks. | | LLM providers | Supported | Anthropic, OpenAI, Gemini, and OpenAI-compatible endpoints. | | Embedding providers | Supported | OpenAI and Voyage. | @@ -72,7 +73,7 @@ priors are at the [bottom](#influences-and-prior-art). mode. Mounted on the same axum server as MCP. - **Multi-agent + multi-machine ready.** Supported clients: Claude Code, Codex, OpenCode, Cursor, Claude Desktop (via `mcp-remote`), - Gemini CLI, OpenClaw, and Oh My Pi / OMP (`pi` / `omp` aliases). + Gemini CLI, Antigravity CLI, OpenClaw, and Oh My Pi / OMP (`pi` / `omp` aliases). Server runs local (loopback) OR on a homelab box (LAN/VPN/cloud) with bearer-token auth. - **Thin-client CLI.** `ai-memory bootstrap`, `purge-project`, @@ -115,7 +116,7 @@ priors are at the [bottom](#influences-and-prior-art). ## Quick start You need: Docker + an agent CLI (Claude Code, Codex, OpenCode, OMP, Cursor, -or anything else that speaks MCP). +Antigravity CLI, or anything else that speaks MCP). The default quick-start has **no authentication** - the server binds to loopback only, so on a single-user laptop nothing else can reach @@ -187,8 +188,8 @@ overwrites them so future image updates ship updated hooks. Drop - **Upgrades:** run `ai-memory upgrade` to refresh the wrapper, image, and staged hook scripts. Redeploy remote servers separately. -For Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, OpenClaw, -curl-based hook installs, source builds, CLI env vars, and the full +For Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, +OpenClaw, curl-based hook installs, source builds, CLI env vars, and the full subcommand reference, see [`docs/install.md`](docs/install.md). ## Security @@ -308,7 +309,7 @@ diagram, crate breakdown, schema notes, and invariants. | [`docs/usage.md`](docs/usage.md) | Handoffs, proactive memory queries, routing snippet, web UI, raw-wiki inspection, and rules-vs-facts workflow. | | [`docs/marker-file.md`](docs/marker-file.md) | `.ai-memory.toml` workspace/project routing for multi-client trees, mono-repos, and work/personal separation. | | [`docs/windows.md`](docs/windows.md) | Windows install modes: full WSL2, native Windows with Docker Desktop, native source builds, and current hook/MCP harness caveats. | -| [`docs/mcp-install.md`](docs/mcp-install.md) | Per-client MCP and lifecycle notes (Cursor, Claude Desktop, Gemini CLI, OpenClaw, OMP). | +| [`docs/mcp-install.md`](docs/mcp-install.md) | Per-client MCP and lifecycle notes (Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, OpenClaw, OMP). | | [`docs/deploy.md`](docs/deploy.md) | Homelab deploy: bin/deploy, bearer-token auth, TLS via cloudflared. | | [`docs/lifecycle-ops.md`](docs/lifecycle-ops.md) | **Read before running purge / rename / backup / restore / reset.** Safety matrix for the state-touching commands, per-project disk layout (how isolation actually works), and operator workflows for "fresh start", "snapshot before risky op", "drop one project". | | [`docs/llm-provider-comparison.md`](docs/llm-provider-comparison.md) | Empirical notes behind the recommended LLM defaults. | diff --git a/crates/ai-memory-cli/src/cli.rs b/crates/ai-memory-cli/src/cli.rs index d239737a..23ca2820 100644 --- a/crates/ai-memory-cli/src/cli.rs +++ b/crates/ai-memory-cli/src/cli.rs @@ -359,6 +359,9 @@ pub enum AgentChoice { /// OpenClaw personal AI gateway — native plugin package with /// session/tool/compaction hooks. Openclaw, + /// Google Antigravity CLI (`agy`) — JSON-config hooks in + /// `~/.gemini/antigravity-cli/hooks.json`. + AntigravityCli, } /// MCP client to render configuration for. Includes both the @@ -389,6 +392,8 @@ pub enum McpClient { /// Oh My Pi (`omp`) / Pi-compatible coding agent — `~/.omp/agent/mcp.json`. #[value(alias = "omp", alias = "oh-my-pi")] Pi, + /// Google Antigravity CLI (`agy`) — `~/.gemini/antigravity-cli/mcp_config.json`. + AntigravityCli, } /// Arguments for `commit`. diff --git a/crates/ai-memory-cli/src/commands/install_hooks.rs b/crates/ai-memory-cli/src/commands/install_hooks.rs index 4276a177..93902ddf 100644 --- a/crates/ai-memory-cli/src/commands/install_hooks.rs +++ b/crates/ai-memory-cli/src/commands/install_hooks.rs @@ -23,8 +23,9 @@ use crate::cli::{AgentChoice, InstallHooksArgs}; use crate::commands::apply_shared::{ApplyOutcome, apply_atomic, mutate_json}; use crate::commands::openclaw_plugin; use crate::commands::render_shared::{ - CURSOR_PROFILE, GEMINI_PROFILE, build_claude_code_payload, build_codex_payload, - build_profile_payload, hook_script_for_current_platform, ts_string_literal, + CURSOR_PROFILE, GEMINI_PROFILE, build_antigravity_payload, build_claude_code_payload, + build_codex_payload, build_profile_payload, hook_script_for_current_platform, + ts_string_literal, }; use crate::config::Config; @@ -58,6 +59,10 @@ pub fn run(config: &Config, args: InstallHooksArgs) -> Result<()> { let hooks_dir = resolve_hooks_dir(args.hooks_dir.as_deref(), args.agent)?; apply_to_gemini_settings(&hooks_dir, &args.server_url, auth, &args) } + AgentChoice::AntigravityCli => { + let hooks_dir = resolve_hooks_dir(args.hooks_dir.as_deref(), args.agent)?; + apply_to_antigravity_settings(&hooks_dir, &args.server_url, auth, &args) + } AgentChoice::Openclaw => openclaw_plugin::apply(&args.server_url, auth, &args), }; } @@ -80,6 +85,10 @@ pub fn run(config: &Config, args: InstallHooksArgs) -> Result<()> { let hooks_dir = resolve_hooks_dir(args.hooks_dir.as_deref(), args.agent)?; render_agent("gemini-cli", &hooks_dir, &args.server_url, auth) } + AgentChoice::AntigravityCli => { + let hooks_dir = resolve_hooks_dir(args.hooks_dir.as_deref(), args.agent)?; + render_agent("antigravity-cli", &hooks_dir, &args.server_url, auth) + } AgentChoice::Openclaw => { openclaw_plugin::render(&args.server_url, auth); Ok(()) @@ -394,6 +403,74 @@ fn merge_gemini_hooks( }) } +/// Mutate `~/.gemini/antigravity-cli/hooks.json` so Antigravity CLI (`agy`) +/// fires the ai-memory scripts on its lifecycle events. +/// +/// Antigravity CLI uses a named-groups format where hook groups are +/// top-level keys (e.g. `"ai-memory"`) containing event arrays. Tool +/// events (`PreToolUse`, `PostToolUse`) use nested shape with matcher; +/// lifecycle events (`PreInvocation`, `Stop`) use flat shape. +/// +/// Config file: `~/.gemini/antigravity-cli/hooks.json` +fn apply_to_antigravity_settings( + hooks_dir: &Path, + server_url: &str, + auth_token: Option<&str>, + args: &InstallHooksArgs, +) -> Result<()> { + let path = match &args.config_file { + Some(p) => p.clone(), + None => dirs::home_dir() + .context("could not locate $HOME for ~/.gemini/antigravity-cli/hooks.json")? + .join(".gemini") + .join("antigravity-cli") + .join("hooks.json"), + }; + let staged = stage_hook_scripts(hooks_dir, "antigravity-cli")?; + let command_dir = staged_command_dir(&staged, "antigravity-cli"); + let outcome = merge_antigravity_hooks(&command_dir, server_url, auth_token, &path)?; + println!( + "✓ {} {} ({})", + outcome.verb(), + path.display(), + match outcome { + ApplyOutcome::Created => "new file", + ApplyOutcome::Updated => "backup written next to it", + ApplyOutcome::NoOp => "already up to date", + } + ); + Ok(()) +} + +fn merge_antigravity_hooks( + staged: &Path, + server_url: &str, + auth_token: Option<&str>, + config_path: &Path, +) -> Result { + let payload = build_antigravity_payload(staged, server_url, auth_token); + let our_group = payload + .get("ai-memory") + .and_then(|v| v.as_object()) + .context("internal: build_antigravity_payload didn't return an ai-memory group")? + .clone(); + apply_atomic(config_path, |existing| { + mutate_json(existing, |root| { + // Get-or-create the "ai-memory" named group; overlay + // our events. Other named groups survive untouched. + let group = root + .entry("ai-memory") + .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())) + .as_object_mut() + .context("`ai-memory` is present in hooks.json but not an object")?; + for (event, value) in &our_group { + group.insert(event.clone(), value.clone()); + } + Ok(()) + }) + }) +} + /// Generate an OpenCode plugin at `~/.config/opencode/plugins/ai-memory.ts`. /// /// OpenCode's integration surface is a TypeScript plugin, not a JSON @@ -1168,6 +1245,7 @@ fn resolve_hooks_dir(explicit: Option<&Path>, agent: AgentChoice) -> Result "codex", AgentChoice::Cursor => "cursor", AgentChoice::GeminiCli => "gemini-cli", + AgentChoice::AntigravityCli => "antigravity-cli", AgentChoice::OpenCode | AgentChoice::Omp | AgentChoice::Openclaw => { anyhow::bail!("{agent:?} uses a generated integration, not a hook script directory") } @@ -1279,7 +1357,14 @@ mod tests { "PowerShell hooks require the shared lib helper" ); - for agent_dir in ["claude-code", "codex", "cursor", "gemini-cli", "opencode"] { + for agent_dir in [ + "claude-code", + "codex", + "cursor", + "gemini-cli", + "opencode", + "antigravity-cli", + ] { let dir = hooks_root.join(agent_dir); let mut sh = BTreeMap::new(); let mut ps1 = BTreeMap::new(); @@ -1810,4 +1895,103 @@ mod tests { "PreToolUse must not appear in Gemini config" ); } + + // ---------------------------------------------------------------- + // Antigravity tests + // ---------------------------------------------------------------- + + #[test] + fn antigravity_preserves_existing_hooks_and_adds_ours() { + let hooks_tmp = TempDir::new().unwrap(); + stub_scripts( + hooks_tmp.path(), + &[ + "session-start.sh", + "session-end.sh", + "pre-tool-use.sh", + "post-tool-use.sh", + ], + ); + + let config_tmp = TempDir::new().unwrap(); + let config_path = config_tmp.path().join("hooks.json"); + // Pre-existing settings with another named hook group. + fs::write( + &config_path, + r#"{"my-linter":{"PostToolUse":[{"matcher":"run_command","hooks":[{"type":"command","command":"lint.sh"}]}]}}"#, + ) + .unwrap(); + + merge_antigravity_hooks( + hooks_tmp.path(), + "http://127.0.0.1:49374", + None, + &config_path, + ) + .unwrap(); + + let parsed: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap(); + // The pre-existing my-linter group survives. + assert!( + parsed["my-linter"]["PostToolUse"].is_array(), + "my-linter.PostToolUse must survive" + ); + // Our named group "ai-memory" is present. + assert!( + parsed["ai-memory"]["PreInvocation"].is_array(), + "PreInvocation hook should be present" + ); + assert!( + parsed["ai-memory"]["PreToolUse"].is_array(), + "PreToolUse hook should be present" + ); + assert!( + parsed["ai-memory"]["PostToolUse"].is_array(), + "PostToolUse hook should be present" + ); + assert!( + parsed["ai-memory"]["Stop"].is_array(), + "Stop hook should be present" + ); + } + + #[test] + fn antigravity_apply_is_idempotent() { + let hooks_tmp = TempDir::new().unwrap(); + stub_scripts( + hooks_tmp.path(), + &[ + "session-start.sh", + "session-end.sh", + "pre-tool-use.sh", + "post-tool-use.sh", + ], + ); + + let config_tmp = TempDir::new().unwrap(); + let config_path = config_tmp.path().join("hooks.json"); + + let first = merge_antigravity_hooks( + hooks_tmp.path(), + "http://127.0.0.1:49374", + None, + &config_path, + ) + .unwrap(); + assert_ne!( + first, + ApplyOutcome::NoOp, + "first apply should not be a no-op" + ); + + let second = merge_antigravity_hooks( + hooks_tmp.path(), + "http://127.0.0.1:49374", + None, + &config_path, + ) + .unwrap(); + assert_eq!(second, ApplyOutcome::NoOp, "second apply must be a no-op"); + } } diff --git a/crates/ai-memory-cli/src/commands/install_instructions.rs b/crates/ai-memory-cli/src/commands/install_instructions.rs index 86e48611..12b13551 100644 --- a/crates/ai-memory-cli/src/commands/install_instructions.rs +++ b/crates/ai-memory-cli/src/commands/install_instructions.rs @@ -92,7 +92,7 @@ fn resolve_targets(explicit: Option<&std::path::PathBuf>) -> Result Result<()> { McpClient::GeminiCli => render_gemini_cli(&args)?, McpClient::Openclaw => render_openclaw(&args)?, McpClient::Pi => render_pi(&args)?, + McpClient::AntigravityCli => render_antigravity_cli(&args)?, }; println!("{snippet}"); Ok(()) @@ -108,6 +109,10 @@ fn resolve_config_file(args: &InstallMcpArgs) -> Result { McpClient::GeminiCli => home.join(".gemini").join("settings.json"), McpClient::Openclaw => home.join(".openclaw").join("config.json"), McpClient::Pi => home.join(".omp").join("agent").join("mcp.json"), + McpClient::AntigravityCli => home + .join(".gemini") + .join("antigravity-cli") + .join("mcp_config.json"), }) } @@ -142,7 +147,8 @@ fn json_mcp_location(client: McpClient) -> Option { | McpClient::ClaudeDesktop | McpClient::Cursor | McpClient::GeminiCli - | McpClient::Pi => Some(JsonMcpLocation::RootMcpServers), + | McpClient::Pi + | McpClient::AntigravityCli => Some(JsonMcpLocation::RootMcpServers), McpClient::OpenCode => Some(JsonMcpLocation::RootMcp), McpClient::Openclaw => Some(JsonMcpLocation::NestedMcpServers), McpClient::Codex => None, @@ -261,6 +267,13 @@ fn build_mcp_entry(args: &InstallMcpArgs) -> Result { entry.insert("headers".into(), json!({"Authorization": b})); } } + McpClient::AntigravityCli => { + entry.insert("serverUrl".into(), json!(args.server_url)); + entry.insert("timeout".into(), json!(GEMINI_MCP_TIMEOUT_MS)); + if let Some(b) = &bearer { + entry.insert("headers".into(), json!({"Authorization": b})); + } + } _ => bail!("internal: build_mcp_entry called for unsupported client"), } Ok(serde_json::Value::Object(entry)) @@ -528,6 +541,17 @@ fn render_pi(args: &InstallMcpArgs) -> Result { )) } +fn render_antigravity_cli(args: &InstallMcpArgs) -> Result { + Ok(format!( + "# Antigravity CLI (`agy`) — merge into ~/.gemini/antigravity-cli/mcp_config.json:\n\ + #\n\ + # Antigravity CLI uses `serverUrl` (not `url` or `httpUrl`) for\n\ + # streamable-HTTP endpoints. The `timeout` is in milliseconds.\n\ + {snippet}\n", + snippet = render_json_mcp_fragment(args)?, + )) +} + #[cfg(test)] mod tests { use super::*; @@ -565,6 +589,7 @@ mod tests { McpClient::GeminiCli => render_gemini_cli(&args).unwrap(), McpClient::Openclaw => render_openclaw(&args).unwrap(), McpClient::Pi => render_pi(&args).unwrap(), + McpClient::AntigravityCli => render_antigravity_cli(&args).unwrap(), } } @@ -581,6 +606,7 @@ mod tests { McpClient::GeminiCli, McpClient::Openclaw, McpClient::Pi, + McpClient::AntigravityCli, ] { let out = render_with_token(client); // Every client embeds the token as `Authorization: @@ -611,6 +637,7 @@ mod tests { McpClient::GeminiCli, McpClient::Openclaw, McpClient::Pi, + McpClient::AntigravityCli, ] { let out = render_for_test(client); assert!( @@ -631,6 +658,7 @@ mod tests { McpClient::GeminiCli => render_gemini_cli(&args).unwrap(), McpClient::Openclaw => render_openclaw(&args).unwrap(), McpClient::Pi => render_pi(&args).unwrap(), + McpClient::AntigravityCli => render_antigravity_cli(&args).unwrap(), } } @@ -646,6 +674,7 @@ mod tests { assert!(render_for_test(McpClient::Openclaw).contains("\"streamable-http\"")); assert!(render_for_test(McpClient::Codex).contains("[mcp_servers.ai-memory]")); assert!(render_for_test(McpClient::Pi).contains("~/.omp/agent/mcp.json")); + assert!(render_for_test(McpClient::AntigravityCli).contains("\"serverUrl\"")); } /// The Codex apply path must emit block-form `[mcp_servers.]` diff --git a/crates/ai-memory-cli/src/commands/render_shared.rs b/crates/ai-memory-cli/src/commands/render_shared.rs index 03048d12..f3add7c3 100644 --- a/crates/ai-memory-cli/src/commands/render_shared.rs +++ b/crates/ai-memory-cli/src/commands/render_shared.rs @@ -199,6 +199,87 @@ pub(crate) const GEMINI_PROFILE: HookProfile = HookProfile { shape: HookShape::Nested, }; +/// Antigravity CLI (`agy`) hook-event vocabulary (per +/// ). Named-groups format +/// at the top level; events inside each group. +/// Tool events use nested shape (matcher + hooks), lifecycle +/// events use flat shape (direct handler list). +pub(crate) const ANTIGRAVITY_TOOL_EVENTS: [(&str, &str); 2] = [ + ("PreToolUse", "pre-tool-use.sh"), + ("PostToolUse", "post-tool-use.sh"), +]; + +pub(crate) const ANTIGRAVITY_LIFECYCLE_EVENTS: [(&str, &str); 2] = [ + ("PreInvocation", "session-start.sh"), + ("Stop", "session-end.sh"), +]; + +/// Build the Antigravity CLI (`agy`) `hooks.json` payload. +/// +/// Antigravity CLI uses a named-groups format where the top level +/// maps hook-group names to their event configurations. Each group +/// can contain any subset of the supported events. Tool events +/// (`PreToolUse`, `PostToolUse`) use the nested shape with matcher; +/// lifecycle events (`PreInvocation`, `Stop`) use a flat handler +/// list where matcher is ignored. +/// +/// The output is `{ "ai-memory": { } }`. +#[must_use] +pub(crate) fn build_antigravity_payload( + emit_root: &Path, + server_url: &str, + auth_token: Option<&str>, +) -> serde_json::Value { + build_antigravity_payload_for_platform( + emit_root, + server_url, + auth_token, + HookCommandPlatform::current(), + ) +} + +fn build_antigravity_payload_for_platform( + emit_root: &Path, + server_url: &str, + auth_token: Option<&str>, + platform: HookCommandPlatform, +) -> serde_json::Value { + let mut group = serde_json::Map::new(); + + // Tool events: nested shape (matcher + inner hooks array) + for (event, script) in &ANTIGRAVITY_TOOL_EVENTS { + let s = script_for_platform(script, platform); + let abs = emit_root.join(s.as_ref()); + let command = hook_command(&abs, server_url, auth_token, platform); + group.insert( + (*event).to_string(), + json!([{ + "matcher": "", + "hooks": [{ + "type": "command", + "command": command, + }], + }]), + ); + } + + // Lifecycle events: flat shape (direct handler list, no matcher) + for (event, script) in &ANTIGRAVITY_LIFECYCLE_EVENTS { + let s = script_for_platform(script, platform); + let abs = emit_root.join(s.as_ref()); + let command = hook_command(&abs, server_url, auth_token, platform); + group.insert( + (*event).to_string(), + json!([{ + "type": "command", + "command": command, + }]), + ); + } + + json!({ "ai-memory": group }) +} + /// Build a Codex-flavoured hook payload. Thin alias for back-compat; /// new code should call `build_profile_payload(&CODEX_PROFILE, …)`. pub(crate) fn build_codex_payload( @@ -637,4 +718,63 @@ mod tests { "Windows command must not use sh: {cmd}" ); } + + #[test] + fn antigravity_payload_uses_named_groups_with_mixed_shape() { + let root = PathBuf::from("/host/hooks/antigravity-cli"); + let v = build_antigravity_payload(&root, "http://localhost:49374", Some("tok")); + + // Top-level key is the named group "ai-memory", not "hooks" + let group = v + .get("ai-memory") + .and_then(|g| g.as_object()) + .expect("missing top-level 'ai-memory' named group"); + assert!( + !v.as_object().unwrap().contains_key("hooks"), + "Antigravity uses named groups, not a 'hooks' wrapper" + ); + + // Tool events: nested shape (matcher + hooks array) + let pre_tool = group + .get("PreToolUse") + .and_then(|e| e.as_array()) + .expect("missing PreToolUse"); + let outer = pre_tool[0].as_object().unwrap(); + assert!(outer.contains_key("matcher")); + let inner = outer.get("hooks").and_then(|h| h.as_array()).unwrap(); + assert_eq!(inner.len(), 1); + let entry = inner[0].as_object().unwrap(); + assert_eq!(entry.get("type").and_then(|t| t.as_str()), Some("command")); + + // Lifecycle events: flat shape (no matcher, direct handler list) + let pre_invocation = group + .get("PreInvocation") + .and_then(|e| e.as_array()) + .expect("missing PreInvocation"); + let handler = pre_invocation[0].as_object().unwrap(); + assert!( + !handler.contains_key("matcher"), + "PreInvocation should not have matcher (flat shape)" + ); + assert!( + !handler.contains_key("hooks"), + "PreInvocation should not have inner hooks array (flat shape)" + ); + assert_eq!( + handler.get("type").and_then(|t| t.as_str()), + Some("command") + ); + + // Auth token inlined into commands + let cmd = handler.get("command").and_then(|c| c.as_str()).unwrap(); + assert!(cmd.contains("AI_MEMORY_AUTH_TOKEN=tok")); + + // All expected events present + for expected in ["PreToolUse", "PostToolUse", "PreInvocation", "Stop"] { + assert!( + group.contains_key(expected), + "missing Antigravity event {expected}" + ); + } + } } diff --git a/crates/ai-memory-cli/src/commands/setup_agent.rs b/crates/ai-memory-cli/src/commands/setup_agent.rs index 762b2119..d16cac81 100644 --- a/crates/ai-memory-cli/src/commands/setup_agent.rs +++ b/crates/ai-memory-cli/src/commands/setup_agent.rs @@ -63,6 +63,7 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> { AgentChoice::Codex => "codex", AgentChoice::Cursor => "cursor", AgentChoice::GeminiCli => "gemini-cli", + AgentChoice::AntigravityCli => "antigravity-cli", AgentChoice::OpenCode => unreachable!("opencode handled above"), AgentChoice::Omp => unreachable!("omp handled above"), AgentChoice::Openclaw => unreachable!("openclaw handled above"), @@ -120,7 +121,10 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> { match args.agent { AgentChoice::ClaudeCode => emit_claude_code(&emit_root, &args)?, - AgentChoice::Codex | AgentChoice::Cursor | AgentChoice::GeminiCli => { + AgentChoice::Codex + | AgentChoice::Cursor + | AgentChoice::GeminiCli + | AgentChoice::AntigravityCli => { emit_other(&emit_root, agent_sub, &args); } AgentChoice::OpenCode => unreachable!("opencode handled above"), diff --git a/crates/ai-memory-core/src/ids.rs b/crates/ai-memory-core/src/ids.rs index 09426643..d3801dcc 100644 --- a/crates/ai-memory-core/src/ids.rs +++ b/crates/ai-memory-core/src/ids.rs @@ -186,6 +186,8 @@ pub enum AgentKind { /// OpenClaw personal AI gateway. #[serde(rename = "openclaw", alias = "open-claw")] OpenClaw, + /// Google Antigravity CLI (`agy`). + AntigravityCli, /// Oh My Pi (`omp`) / Pi-compatible coding agent. Omp, /// Anything else (manual capture, future agents). @@ -204,6 +206,7 @@ impl AgentKind { Self::GeminiCli => "gemini-cli", Self::ClaudeDesktop => "claude-desktop", Self::OpenClaw => "openclaw", + Self::AntigravityCli => "antigravity-cli", Self::Omp => "omp", Self::Other => "other", } @@ -222,6 +225,7 @@ impl AgentKind { "gemini-cli" | "gemini" => Self::GeminiCli, "claude-desktop" | "claude_desktop" => Self::ClaudeDesktop, "openclaw" | "open-claw" => Self::OpenClaw, + "antigravity-cli" | "antigravity" | "agy" => Self::AntigravityCli, "omp" | "pi" | "oh-my-pi" => Self::Omp, _ => Self::Other, } diff --git a/crates/ai-memory-mcp/src/server.rs b/crates/ai-memory-mcp/src/server.rs index 334b0f33..ef123cc2 100644 --- a/crates/ai-memory-mcp/src/server.rs +++ b/crates/ai-memory-mcp/src/server.rs @@ -792,6 +792,7 @@ impl AiMemoryServer { "opencode": "AGENTS.md", "cursor": "AGENTS.md", "gemini_cli": "AGENTS.md", + "antigravity_cli": "AGENTS.md", "default": "AGENTS.md" }, "notes": [ diff --git a/docs/install.md b/docs/install.md index 272fb4b4..ca9eb270 100644 --- a/docs/install.md +++ b/docs/install.md @@ -7,7 +7,7 @@ path (docker + Claude Code). This page covers everything else: (homelab, LAN box, remote server) - [Configuring the CLI URL and auth](#configuring-the-cli-url-and-auth) - [Configuring other agent CLIs](#configuring-other-agent-clis) - (Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, OpenClaw) + (Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, OpenClaw) - [Installing hooks without docker](#installing-hooks-without-docker) (curl-based installer) - [Running ai-memory without docker](#running-ai-memory-without-docker) @@ -116,11 +116,11 @@ Each agent CLI needs two things: becomes manual. Claude Desktop is MCP-only today. Claude Code, Codex, OpenCode, OMP, -Cursor, Gemini CLI, and OpenClaw have lifecycle capture paths through +Cursor, Gemini CLI, Antigravity CLI, and OpenClaw have lifecycle capture paths through `install-hooks`. -> **Two-step hook install pattern.** Claude Code, Codex, Cursor, and -> Gemini CLI use shell/PowerShell hook scripts: (1) `docker cp` the +> **Two-step hook install pattern.** Claude Code, Codex, Cursor, +> Gemini CLI, and Antigravity CLI use shell/PowerShell hook scripts: (1) `docker cp` the > bundled scripts to your home dir, (2) `docker run --rm install-hooks` > to render the config snippet. > OpenClaw, OpenCode, and OMP are different: they use generated @@ -214,7 +214,7 @@ files owned by the user running the command. Prefer it as the default; reach for `setup-agent` only when your docker setup is known not to remap UIDs. -### Cursor, Gemini CLI, Claude Desktop, OpenClaw +### Cursor, Gemini CLI, Claude Desktop, OpenClaw, Antigravity CLI See [**`docs/mcp-install.md`**](mcp-install.md) for the per-client MCP config file path and snippet, or one-shot it via: @@ -249,7 +249,7 @@ docker run --rm akitaonrails/ai-memory:latest \ --server-url "http://homelab:49374" ``` -Cursor, Gemini CLI, and OpenClaw support both `install-mcp` and +Cursor, Gemini CLI, Antigravity CLI, and OpenClaw support both `install-mcp` and `install-hooks`. Claude Desktop is MCP-only here, so you'll need to nudge the model to call `memory_query` / `memory_handoff_accept` itself. For clients with `install-hooks` support, the capture path handles @@ -277,7 +277,7 @@ docker run --rm akitaonrails/ai-memory:latest \ ``` The curl script installer supports -`--agent claude-code|codex|cursor|gemini-cli|opencode|openclaw|omp|pi` +`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi` and `--to `; `--help` prints the full flag list. OpenCode, OpenClaw, and OMP do not need script extraction because `install-hooks` generates TypeScript plugin/extension files for them instead. @@ -584,6 +584,6 @@ write to `~/.local/share/ai-memory/hooks/`. - [`docs/usage.md`](usage.md) - handoffs, proactive querying, web UI, routing snippet, and raw-wiki inspection - [`docs/mcp-install.md`](mcp-install.md) - per-client MCP config - reference for Cursor, Claude Desktop, Gemini CLI, OpenClaw, OMP + reference for Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, OpenClaw, OMP - [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) - what's actually running inside ai-memory diff --git a/docs/mcp-install.md b/docs/mcp-install.md index 8432244e..5de0112b 100644 --- a/docs/mcp-install.md +++ b/docs/mcp-install.md @@ -19,9 +19,9 @@ This page documents how to register ai-memory as an MCP server with agent CLIs beyond the README quick start. -Claude Code, OpenAI Codex, Cursor, Gemini CLI, OpenClaw, OpenCode, and +Claude Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, OpenClaw, OpenCode, and OMP have automatic capture integrations (shell/PowerShell hooks for -Claude Code / Codex / Cursor / Gemini CLI, TypeScript plugin/extension +Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI, TypeScript plugin/extension files for OpenClaw / OpenCode / OMP) and are covered in the [main README](../README.md#quick-start). @@ -41,7 +41,7 @@ the LLM to call `memory_handoff_begin` manually before quitting. > **One-shot tip:** every snippet below is also reachable from the > CLI: > ```bash -> ai-memory install-mcp --client gemini-cli # or cursor / claude-desktop / openclaw / pi|omp +> ai-memory install-mcp --client gemini-cli # or cursor / claude-desktop / openclaw / pi|omp / antigravity-cli > ``` --- @@ -166,6 +166,102 @@ capture path; `SessionStart` also fetches pending handoffs. --- +## Antigravity CLI (`agy`) + +**Status:** ✅ MCP supported. ✅ Lifecycle hooks supported via +`ai-memory install-hooks --agent antigravity-cli --apply`. + +**Config file (MCP):** `~/.gemini/antigravity-cli/mcp_config.json` + +Antigravity CLI is the successor to Gemini CLI, built in Go with +parallel subagent support. It uses a separate `mcp_config.json` +(instead of Gemini CLI's combined `settings.json`) and uses +`serverUrl` (not `httpUrl`) for streamable-HTTP endpoints. + +```bash +# One-shot via CLI: +ai-memory install-mcp --client antigravity-cli +``` + +The rendered snippet writes to `mcp_config.json` under `mcpServers`: + +```json +{ + "mcpServers": { + "ai-memory": { + "serverUrl": "http://127.0.0.1:49374/mcp", + "timeout": 5000 + } + } +} +``` + +**Config file (hooks):** `~/.gemini/antigravity-cli/hooks.json` + +Antigravity CLI uses a named-groups hook format. Each top-level key +is a hook group name; inside, event arrays map to handlers. Tool +events (`PreToolUse`, `PostToolUse`) use nested shape with matcher; +lifecycle events (`PreInvocation`, `Stop`) use flat shape. + +```bash +# One-shot via CLI: +ai-memory install-hooks --agent antigravity-cli --apply +``` + +The rendered hooks config looks like: + +```json +{ + "ai-memory": { + "PreInvocation": [ + { + "type": "command", + "command": "AI_MEMORY_HOOK_URL=http://127.0.0.1:49374 /path/to/session-start.sh" + } + ], + "PreToolUse": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "AI_MEMORY_HOOK_URL=http://127.0.0.1:49374 /path/to/pre-tool-use.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "AI_MEMORY_HOOK_URL=http://127.0.0.1:49374 /path/to/post-tool-use.sh" + } + ] + } + ], + "Stop": [ + { + "type": "command", + "command": "AI_MEMORY_HOOK_URL=http://127.0.0.1:49374 /path/to/session-end.sh" + } + ] + } +} +``` + +**Gotchas:** +- Antigravity CLI uses `serverUrl` for HTTP MCP endpoints, not `url` + or `httpUrl`. The `--apply` flag writes the correct key. +- Hook scripts are staged under `~/.local/share/ai-memory/hooks/antigravity-cli/`. +- The `PreInvocation` event fires before each model call (not just at + session start). This is the closest equivalent to Gemini CLI's + `SessionStart`. +- Source: + +--- + ## OpenClaw **Status:** ✅ MCP supported. ✅ Lifecycle hooks supported via a native @@ -300,8 +396,8 @@ that *starts* the next one - to play nicely with ai-memory: | Side | What's needed | Covered by | |---|---|---| -| **Ending side** | The agent must create a handoff, either through a true session-end hook or by calling `memory_handoff_begin`. | Built-in for Claude Code, Codex, Cursor, Gemini CLI, OpenClaw, and OMP. OpenCode has no true session-end event, so ask it to call `memory_handoff_begin` before quitting when you need a handoff. | -| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model proactively calls `memory_handoff_accept` on first turn. | (a) is built-in for Claude Code / Codex / Cursor / Gemini CLI / OpenClaw / OpenCode / OMP. (b) works for any MCP-capable client if you nudge the model - see [the routing snippet](usage.md#install-the-routing-snippet). | +| **Ending side** | The agent must create a handoff, either through a true session-end hook or by calling `memory_handoff_begin`. | Built-in for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI, OpenClaw, and OMP. OpenCode has no true session-end event, so ask it to call `memory_handoff_begin` before quitting when you need a handoff. | +| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model proactively calls `memory_handoff_accept` on first turn. | (a) is built-in for Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI / OpenClaw / OpenCode / OMP. (b) works for any MCP-capable client if you nudge the model - see [the routing snippet](usage.md#install-the-routing-snippet). | So a typical mixed workflow looks like: diff --git a/hooks/_lib.sh b/hooks/_lib.sh index 787f80e1..d9654749 100644 --- a/hooks/_lib.sh +++ b/hooks/_lib.sh @@ -2,7 +2,7 @@ # Sourced by per-agent lifecycle hook scripts. POSIX shell only — # no bash-isms, no external deps (no jq, no toml crate). Keep changes # byte-trivial because every supported agent (claude-code, codex, -# cursor, gemini-cli, opencode, omp) sources this same file. +# cursor, gemini-cli, antigravity-cli, opencode, omp) sources this same file. # Walk up from "$1" toward $HOME (or /) looking for `.ai-memory.toml`. # Prints the absolute path of the first marker found, or nothing. diff --git a/hooks/antigravity-cli/post-tool-use.ps1 b/hooks/antigravity-cli/post-tool-use.ps1 new file mode 100644 index 00000000..1d615041 --- /dev/null +++ b/hooks/antigravity-cli/post-tool-use.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "post-tool-use" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/post-tool-use.sh b/hooks/antigravity-cli/post-tool-use.sh new file mode 100755 index 00000000..1b9bdf98 --- /dev/null +++ b/hooks/antigravity-cli/post-tool-use.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli post-tool-use hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=post-tool-use&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/antigravity-cli/pre-compact.ps1 b/hooks/antigravity-cli/pre-compact.ps1 new file mode 100644 index 00000000..35a9f03f --- /dev/null +++ b/hooks/antigravity-cli/pre-compact.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "pre-compact" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/pre-compact.sh b/hooks/antigravity-cli/pre-compact.sh new file mode 100755 index 00000000..b874cb79 --- /dev/null +++ b/hooks/antigravity-cli/pre-compact.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli pre-compact hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=pre-compact&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/antigravity-cli/pre-tool-use.ps1 b/hooks/antigravity-cli/pre-tool-use.ps1 new file mode 100644 index 00000000..3b979396 --- /dev/null +++ b/hooks/antigravity-cli/pre-tool-use.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "pre-tool-use" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/pre-tool-use.sh b/hooks/antigravity-cli/pre-tool-use.sh new file mode 100755 index 00000000..e436ed80 --- /dev/null +++ b/hooks/antigravity-cli/pre-tool-use.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli pre-tool-use hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=pre-tool-use&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/antigravity-cli/session-end.ps1 b/hooks/antigravity-cli/session-end.ps1 new file mode 100644 index 00000000..d0fe1dc7 --- /dev/null +++ b/hooks/antigravity-cli/session-end.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "session-end" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/session-end.sh b/hooks/antigravity-cli/session-end.sh new file mode 100755 index 00000000..1331ecfc --- /dev/null +++ b/hooks/antigravity-cli/session-end.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli session-end hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=session-end&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/antigravity-cli/session-start.ps1 b/hooks/antigravity-cli/session-start.ps1 new file mode 100644 index 00000000..9b8abe45 --- /dev/null +++ b/hooks/antigravity-cli/session-start.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "session-start" -Agent "antigravity-cli" -FetchHandoff +exit 0 diff --git a/hooks/antigravity-cli/session-start.sh b/hooks/antigravity-cli/session-start.sh new file mode 100755 index 00000000..fd28a687 --- /dev/null +++ b/hooks/antigravity-cli/session-start.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# antigravity-cli SessionStart hook. +# 1. Forwards the event JSON to the ai-memory server (fire-and-forget). +# 2. Synchronously fetches any pending cross-agent handoff and prints +# it to stdout — agent CLIs prepend session-start hook stdout to +# the next session, so the resuming agent sees prior context with +# no human in the loop. +# +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=session-start&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +ai_memory_get_handoff "$SERVER/handoff?agent=antigravity-cli${QS}" 2>/dev/null || true +exit 0 diff --git a/hooks/antigravity-cli/stop.ps1 b/hooks/antigravity-cli/stop.ps1 new file mode 100644 index 00000000..4a0d6563 --- /dev/null +++ b/hooks/antigravity-cli/stop.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "stop" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/stop.sh b/hooks/antigravity-cli/stop.sh new file mode 100755 index 00000000..2a4be75c --- /dev/null +++ b/hooks/antigravity-cli/stop.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli stop hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=stop&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/antigravity-cli/user-prompt-submit.ps1 b/hooks/antigravity-cli/user-prompt-submit.ps1 new file mode 100644 index 00000000..59246ce7 --- /dev/null +++ b/hooks/antigravity-cli/user-prompt-submit.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "user-prompt" -Agent "antigravity-cli" +exit 0 diff --git a/hooks/antigravity-cli/user-prompt-submit.sh b/hooks/antigravity-cli/user-prompt-submit.sh new file mode 100755 index 00000000..09385fd2 --- /dev/null +++ b/hooks/antigravity-cli/user-prompt-submit.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# antigravity-cli user-prompt hook. +# Forwards the event JSON to the ai-memory server, fire-and-forget. +_lib_dir="$(dirname "$0")" +[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.." +. "$_lib_dir/_lib.sh" + +SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}" +PAYLOAD=$(cat) +CWD=$(ai_memory_extract_cwd "$PAYLOAD") +QS=$(ai_memory_marker_qs "$CWD") + +printf '%s' "$PAYLOAD" \ + | ai_memory_post_hook "$SERVER/hook?event=user-prompt&agent=antigravity-cli${QS}" >/dev/null 2>&1 || true +exit 0 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index c67f6ea4..a2592eaf 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -10,7 +10,7 @@ # | bash -s -- --agent claude-code # # Options: -# --agent +# --agent # which agent (default: claude-code; # generated-plugin agents print hints) # --to install root (default: $HOME/.ai-memory/hooks) @@ -46,9 +46,9 @@ while [[ $# -gt 0 ]]; do done case "$AGENT" in - claude-code|codex|cursor|gemini-cli|opencode|openclaw|omp|pi|oh-my-pi) ;; + claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi|oh-my-pi) ;; *) - echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | opencode | openclaw | omp | pi | oh-my-pi)" >&2 + echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | antigravity-cli | opencode | openclaw | omp | pi | oh-my-pi)" >&2 exit 64 ;; esac