feat: add Antigravity CLI (agy) as supported harness

Antigravity CLI is the successor to Gemini CLI, built in Go with
parallel subagent support. This adds full MCP + lifecycle hook
integration with the correct config format differences:

- MCP config lives in a separate mcp_config.json (not settings.json)
- Uses serverUrl (not httpUrl) for streamable-HTTP endpoints
- Hooks config uses named-groups format with Antigravity-native
  events (PreToolUse, PostToolUse, PreInvocation, Stop)
- Mixed shape: nested for tool events, flat for lifecycle events
This commit is contained in:
Lucas Oliveira
2026-05-25 18:47:28 -03:00
parent 7cd18f20d6
commit 31fc4fe033
27 changed files with 624 additions and 28 deletions
+6 -5
View File
@@ -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. |
+5
View File
@@ -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`.
@@ -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<ApplyOutcome> {
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<Path
AgentChoice::Codex => "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");
}
}
@@ -92,7 +92,7 @@ fn resolve_targets(explicit: Option<&std::path::PathBuf>) -> Result<Vec<std::pat
eprintln!(
"note: neither CLAUDE.md nor AGENTS.md exists in {}; \
creating CLAUDE.md. If you use Codex / OpenCode / \
Cursor / Gemini CLI, re-run with `--target AGENTS.md`.",
Cursor / Gemini CLI / Antigravity CLI, re-run with `--target AGENTS.md`.",
cwd.display()
);
Ok(vec![claude_md])
@@ -55,6 +55,7 @@ pub fn run(config: &Config, args: InstallMcpArgs) -> 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<PathBuf> {
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<JsonMcpLocation> {
| 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<serde_json::Value> {
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<String> {
))
}
fn render_antigravity_cli(args: &InstallMcpArgs) -> Result<String> {
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.<name>]`
@@ -199,6 +199,87 @@ pub(crate) const GEMINI_PROFILE: HookProfile = HookProfile {
shape: HookShape::Nested,
};
/// Antigravity CLI (`agy`) hook-event vocabulary (per
/// <https://antigravity.google/docs/hooks>). 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": { <events> } }`.
#[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}"
);
}
}
}
@@ -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"),
+4
View File
@@ -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,
}
+1
View File
@@ -792,6 +792,7 @@ impl AiMemoryServer {
"opencode": "AGENTS.md",
"cursor": "AGENTS.md",
"gemini_cli": "AGENTS.md",
"antigravity_cli": "AGENTS.md",
"default": "AGENTS.md"
},
"notes": [
+8 -8
View File
@@ -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 <dir>`; `--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
+101 -5
View File
@@ -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: <https://antigravity.google/docs/hooks>
---
## 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:
+1 -1
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "post-tool-use" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "pre-compact" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "pre-tool-use" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "session-end" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "session-start" -Agent "antigravity-cli" -FetchHandoff
exit 0
+21
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "stop" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
@@ -0,0 +1,3 @@
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
Invoke-AiMemoryHook -Event "user-prompt" -Agent "antigravity-cli"
exit 0
+15
View File
@@ -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
+3 -3
View File
@@ -10,7 +10,7 @@
# | bash -s -- --agent claude-code
#
# Options:
# --agent <claude-code|codex|cursor|gemini-cli|opencode|openclaw|omp|pi>
# --agent <claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi>
# which agent (default: claude-code;
# generated-plugin agents print hints)
# --to <dir> 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