diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cffc2a..1dfcbb7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `install-hooks --agent grok` (plus `setup-agent` / `uninstall` coverage) for the xAI **Grok Build CLI**. Grok's `~/.grok/hooks/ai-memory.json` shares Claude - Code's JSON shape and seven-event vocabulary, so it reuses the claude-code hook - scripts and emits the native `ai-memory hook --event … --agent grok` command. + Code's JSON shape and seven-event vocabulary, with a Grok-specific hook bundle + and native `ai-memory hook --event … --agent grok` commands. ai-memory entries merge into a dedicated `ai-memory.json`, leaving any third-party `~/.grok/hooks/*.json` untouched. NOTE: Grok ignores hook stdout on `SessionStart`, so capture works but handoff injection does not. Grok's diff --git a/README.md b/README.md index 220fab9a..aeffa039 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ | 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 (`agy` alias). | -| Grok Build CLI | Hooks | Lifecycle hooks via `install-hooks --agent grok` (`~/.grok/hooks/ai-memory.json`, native `--agent grok`, reuses claude-code scripts). Capture works; no handoff injection — Grok ignores `SessionStart` stdout, so recover handoffs via MCP `memory_handoff_accept`. | +| Grok Build CLI | Hooks | Lifecycle hooks via `install-hooks --agent grok` (`~/.grok/hooks/ai-memory.json`, Grok-specific hook bundle, native `--agent grok`). Capture works; no handoff injection — Grok ignores `SessionStart` stdout, so recover handoffs via MCP `memory_handoff_accept`. | | VS Code Copilot | MCP-only | `.vscode/mcp.json` for Copilot agent mode; no lifecycle hooks (Copilot does not expose them yet). | | LLM/auth providers | Supported | Anthropic, OpenAI, OpenAI OAuth/Codex, GitHub Copilot, Gemini, OpenAI-compatible endpoints, and generic OIDC device auth for native hooks. | | Embedding providers | Supported | OpenAI, Voyage, and Google Gemini. | @@ -101,9 +101,10 @@ priors are at the [bottom](#influences-and-prior-art). ## Use cases - **"Quit at 4 PM, pick up at 9 AM in a different agent."** The - classic. SessionStart hook in the next agent (any of the - supported CLIs) prepends a typed handoff with the open questions, - next steps, and a session summary. + classic. SessionStart hook in the next supported hook client prepends a + typed handoff with open questions, next steps, and a session summary. Grok + captures lifecycle events but ignores SessionStart stdout, so ask it to call + `memory_handoff_accept` when resuming from a handoff. - **"What did we decide about X six weeks ago?"** Type `memory_query X` from the agent (or `ai-memory search X` from a terminal) - FTS5 over the wiki. Pages are LLM-consolidated, so @@ -178,7 +179,7 @@ packaged unit. Full user-service, system-service, auth, and provider setup is in ### Docker You need: Docker + an agent CLI (Claude Code, Codex, OpenCode, OMP, Cursor, -Antigravity CLI, or anything else that speaks MCP). +Antigravity CLI, Grok Build CLI, or anything else that speaks MCP). The published Docker image includes `linux/amd64` and `linux/arm64` variants, so Apple Silicon Macs and ARM64 Linux hosts can pull `akitaonrails/ai-memory` @@ -272,8 +273,8 @@ use `--mcp-url` if you installed MCP with a custom endpoint, and and staged hook scripts. Redeploy remote servers separately. For Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, -OpenClaw, VS Code Copilot, curl-based hook installs, source builds, CLI env vars, -and the full subcommand reference, see [`docs/install.md`](docs/install.md). +Grok Build CLI, OpenClaw, VS Code Copilot, curl-based hook installs, source builds, +CLI env vars, and the full subcommand reference, see [`docs/install.md`](docs/install.md). ## Security diff --git a/crates/ai-memory-cli/src/cli.rs b/crates/ai-memory-cli/src/cli.rs index bfa1e22c..089a1eeb 100644 --- a/crates/ai-memory-cli/src/cli.rs +++ b/crates/ai-memory-cli/src/cli.rs @@ -716,8 +716,8 @@ pub enum AgentChoice { AntigravityCli, /// xAI Grok Build CLI — JSON-config hooks in /// `~/.grok/hooks/ai-memory.json`. Native `ai-memory hook --event` - /// integration like Claude Code (reuses the claude-code hook - /// scripts). NOTE: Grok ignores hook stdout on `SessionStart`, so + /// integration using Grok-specific hook scripts. NOTE: Grok ignores + /// hook stdout on `SessionStart`, so /// capture works but handoff injection does not — recover the prior /// session's handoff via the MCP `memory_handoff_accept` tool. Grok, diff --git a/crates/ai-memory-cli/src/commands/install_hooks.rs b/crates/ai-memory-cli/src/commands/install_hooks.rs index 9c23917a..770e5f5a 100644 --- a/crates/ai-memory-cli/src/commands/install_hooks.rs +++ b/crates/ai-memory-cli/src/commands/install_hooks.rs @@ -509,14 +509,13 @@ fn apply_to_claude_code_settings( Ok(()) } -/// Mutate `~/.grok/hooks/ai-memory.json` so Grok Build CLI fires the -/// ai-memory lifecycle hooks. Grok's hook config is structurally -/// identical to Claude Code's (`{"hooks":{"SessionStart":[{"hooks": -/// [{"type":"command","command":"..."}]}]}}`) and uses the same seven -/// CamelCase event names, so we reuse Claude Code's staged scripts and -/// only swap the emitted `--agent grok` tag. We merge into a dedicated -/// `ai-memory.json` (Grok discovers every `~/.grok/hooks/*.json`), so a -/// pre-existing third-party hook file is left untouched. +/// Mutate `~/.grok/hooks/ai-memory.json` so Grok Build CLI fires the ai-memory +/// lifecycle hooks. Grok's hook config is structurally identical to Claude +/// Code's nested hook JSON and uses the same seven CamelCase event names, but +/// its script bundle carries `agent=grok` and skips destructive SessionStart +/// handoff fetches. We merge into a dedicated `ai-memory.json` (Grok discovers +/// every `~/.grok/hooks/*.json`), so a pre-existing third-party hook file is +/// left untouched. fn apply_to_grok_settings( hooks_dir: &Path, server_url: &str, @@ -1718,9 +1717,7 @@ fn resolve_hooks_dir(explicit: Option<&Path>, agent: AgentChoice) -> Result "cursor", AgentChoice::GeminiCli => "gemini-cli", AgentChoice::AntigravityCli => "antigravity-cli", - // Grok reuses Claude Code's hook scripts (identical JSON shape + - // event vocabulary); only the emitted `--agent grok` tag differs. - AgentChoice::Grok => "claude-code", + AgentChoice::Grok => "grok", AgentChoice::OpenCode | AgentChoice::Omp | AgentChoice::Openclaw => { anyhow::bail!("{agent:?} uses a generated integration, not a hook script directory") } @@ -2100,6 +2097,16 @@ mod tests { ); } + #[test] + fn resolve_hooks_dir_uses_grok_bundle_for_grok() { + let tmp = TempDir::new().unwrap(); + fs::create_dir_all(tmp.path().join("grok")).unwrap(); + fs::create_dir_all(tmp.path().join("claude-code")).unwrap(); + + let resolved = resolve_hooks_dir(Some(tmp.path()), AgentChoice::Grok).unwrap(); + assert_eq!(resolved, tmp.path().join("grok")); + } + #[test] fn opencode_mcp_inference_supplies_hook_origin_and_token() { let inferred = infer_json_mcp_config( diff --git a/crates/ai-memory-cli/src/commands/render_shared.rs b/crates/ai-memory-cli/src/commands/render_shared.rs index 2e1a2a40..9fa6376a 100644 --- a/crates/ai-memory-cli/src/commands/render_shared.rs +++ b/crates/ai-memory-cli/src/commands/render_shared.rs @@ -26,7 +26,7 @@ use serde_json::json; /// /// Adding a hook event means updating this list AND adding the /// matching `.sh` and `.ps1` files under -/// `hooks/{claude-code,codex,cursor,gemini-cli,opencode}/`. The +/// `hooks/{claude-code,codex,cursor,gemini-cli,grok,opencode}/`. The /// install-hooks parity test fails if the bundle drifts. pub(crate) const CLAUDE_CODE_EVENTS: [(&str, &str); 7] = [ ("SessionStart", "session-start.sh"), @@ -131,10 +131,28 @@ pub(crate) fn build_claude_code_payload_with_data_dir( ) } -/// Grok Build CLI hook payload. Grok's `~/.grok/hooks/*.json` shares -/// Claude Code's JSON shape and seven-event vocabulary -/// (`CLAUDE_CODE_EVENTS`, `HookShape::Nested`), so we reuse both and -/// only swap the `--agent grok` tag in the emitted native command. +/// Grok Build CLI hook payload for docker/setup-agent script snippets. +/// Grok shares Claude Code's JSON shape and event vocabulary, but uses +/// its own script bundle so script fallback keeps `agent=grok` and never +/// destructively fetches handoffs on SessionStart. +#[must_use] +pub(crate) fn build_grok_payload( + emit_root: &Path, + server_url: &str, + auth_token: Option<&str>, +) -> serde_json::Value { + build_hook_payload_for_platform( + &CLAUDE_CODE_EVENTS, + emit_root, + server_url, + auth_token, + HookShape::Nested, + HookCommandContext::new(HookCommandPlatform::for_bash_script_runner(), "grok", None), + ) +} + +/// Grok Build CLI hook payload for apply/render paths. Native commands are the +/// default; explicit script fallback still points at the Grok script bundle. pub(crate) fn build_grok_payload_with_data_dir( emit_root: &Path, server_url: &str, @@ -767,6 +785,47 @@ mod tests { } } + #[test] + fn grok_native_payload_uses_grok_agent() { + let root = PathBuf::from("/host/hooks/grok"); + let v = build_hook_payload_for_platform( + &CLAUDE_CODE_EVENTS, + &root, + "http://localhost:49374", + None, + HookShape::Nested, + HookCommandContext::new(HookCommandPlatform::PosixNative, "grok", None), + ); + let command = v + .pointer("/hooks/SessionStart/0/hooks/0/command") + .and_then(|s| s.as_str()) + .unwrap(); + assert!(command.contains("--agent grok"), "{command}"); + assert!(!command.contains("claude-code"), "{command}"); + } + + #[test] + fn grok_script_payload_uses_grok_bundle() { + let root = PathBuf::from("/host/hooks/grok"); + let v = build_hook_payload_for_platform( + &CLAUDE_CODE_EVENTS, + &root, + "http://localhost:49374", + None, + HookShape::Nested, + HookCommandContext::new(HookCommandPlatform::Posix, "grok", None), + ); + let command = v + .pointer("/hooks/SessionStart/0/hooks/0/command") + .and_then(|s| s.as_str()) + .unwrap(); + assert!( + command.contains("/host/hooks/grok/session-start.sh"), + "{command}" + ); + assert!(!command.contains("claude-code"), "{command}"); + } + #[test] fn claude_code_payload_embeds_auth_token_when_provided() { let root = PathBuf::from("/host/hooks/claude-code"); diff --git a/crates/ai-memory-cli/src/commands/setup_agent.rs b/crates/ai-memory-cli/src/commands/setup_agent.rs index 843ee96a..21ad0b31 100644 --- a/crates/ai-memory-cli/src/commands/setup_agent.rs +++ b/crates/ai-memory-cli/src/commands/setup_agent.rs @@ -36,7 +36,8 @@ use anyhow::{Context, Result, bail}; use crate::cli::{AgentChoice, SetupAgentArgs}; use crate::commands::render_shared::{ - CLAUDE_CODE_EVENTS, build_claude_code_payload, hook_script_for_current_platform, + CLAUDE_CODE_EVENTS, build_claude_code_payload, build_grok_payload, + hook_script_for_current_platform, }; use crate::config::{Config, DEFAULT_SERVER_URL}; @@ -70,8 +71,7 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> { AgentChoice::Cursor => "cursor", AgentChoice::GeminiCli => "gemini-cli", AgentChoice::AntigravityCli => "antigravity-cli", - // Grok reuses Claude Code's hook scripts (same shape + events). - AgentChoice::Grok => "claude-code", + AgentChoice::Grok => "grok", AgentChoice::OpenCode => unreachable!("opencode handled above"), AgentChoice::Omp => unreachable!("omp handled above"), AgentChoice::Openclaw => unreachable!("openclaw handled above"), @@ -128,7 +128,8 @@ pub fn run(config: &Config, args: SetupAgentArgs) -> Result<()> { .join(agent_sub); match args.agent { - AgentChoice::ClaudeCode | AgentChoice::Grok => emit_claude_code(&emit_root, &args)?, + AgentChoice::ClaudeCode => emit_claude_code(&emit_root, &args)?, + AgentChoice::Grok => emit_grok(&emit_root, &args)?, AgentChoice::Codex | AgentChoice::Cursor | AgentChoice::GeminiCli @@ -205,6 +206,26 @@ fn emit_claude_code(emit_root: &Path, args: &SetupAgentArgs) -> Result<()> { Ok(()) } +fn emit_grok(emit_root: &Path, args: &SetupAgentArgs) -> Result<()> { + let payload = build_grok_payload(emit_root, &args.server_url, args.auth_token.as_deref()); + let serialized = + serde_json::to_string_pretty(&payload).context("serializing Grok hook config")?; + println!("# Grok Build CLI — write to ~/.grok/hooks/ai-memory.json"); + println!("# Hook scripts (must be reachable from the host that runs Grok):"); + println!("# {}", emit_root.display()); + println!("# AI-memory server: {}", args.server_url); + if args.auth_token.is_some() { + println!("# Auth: AI_MEMORY_AUTH_TOKEN embedded in each hook command below."); + println!("# Treat ~/.grok/hooks/ai-memory.json as sensitive (chmod 600)."); + } + println!("# NOTE: Grok ignores SessionStart stdout, so this config captures"); + println!("# lifecycle events but does not inject handoffs automatically."); + println!("# Recover handoffs via the MCP memory_handoff_accept tool."); + println!(); + println!("{serialized}"); + Ok(()) +} + fn emit_other(emit_root: &Path, label: &str, args: &SetupAgentArgs) { // These clients have hook surfaces, but their print-mode config // snippets are intentionally conservative: apply-mode owns the diff --git a/crates/ai-memory-core/src/ids.rs b/crates/ai-memory-core/src/ids.rs index 4f1e2266..b9fa6f31 100644 --- a/crates/ai-memory-core/src/ids.rs +++ b/crates/ai-memory-core/src/ids.rs @@ -246,11 +246,12 @@ impl AgentKind { /// **destructive** (the server marks the handoff accepted) and the result /// would be discarded — silently losing the handoff. For such agents the /// handoff stays available on demand via the MCP `memory_handoff_accept` - /// tool. Only the native-hook agents (claude-code, grok) reach this code; - /// the others inject via their own staged-script / plugin formats. + /// tool. Unknown future agents return `false` until we know their + /// SessionStart stdout semantics; accepting a handoff is single-use and + /// should fail safe. #[must_use] pub fn session_start_injects_handoff(self) -> bool { - !matches!(self, Self::Grok) + !matches!(self, Self::Grok | Self::Other) } } @@ -296,6 +297,7 @@ mod tests { assert!(!AgentKind::Grok.session_start_injects_handoff()); assert!(AgentKind::ClaudeCode.session_start_injects_handoff()); assert!(AgentKind::Codex.session_start_injects_handoff()); + assert!(!AgentKind::Other.session_start_injects_handoff()); } #[test] diff --git a/crates/ai-memory-store/src/ops.rs b/crates/ai-memory-store/src/ops.rs index e444ee64..d7daba64 100644 --- a/crates/ai-memory-store/src/ops.rs +++ b/crates/ai-memory-store/src/ops.rs @@ -2334,6 +2334,129 @@ mod tests { assert_eq!(parent_rows, 1); } + #[test] + fn v20_adds_grok_and_preserves_sessions_invariants_on_upgraded_db() { + use ai_memory_core::{AgentKind, NewObservation, NewSession, ObservationKind, SessionId}; + + let tmp = TempDir::new().unwrap(); + let db_path = tmp.path().join("test.sqlite"); + let ws; + let proj; + let existing_sid = SessionId::new(); + + { + let mut conn = Connection::open(&db_path).unwrap(); + conn.pragma_update(None, "foreign_keys", "OFF").unwrap(); + crate::migrations::run_to(&mut conn, 19).unwrap(); + conn.pragma_update(None, "foreign_keys", "ON").unwrap(); + ws = get_or_create_workspace(&mut conn, "default").unwrap(); + proj = get_or_create_project(&mut conn, &ws, "scratch", None).unwrap(); + begin_session( + &mut conn, + &NewSession { + id: existing_sid, + workspace_id: ws, + project_id: proj, + agent_kind: AgentKind::ClaudeCode, + cwd: None, + }, + ) + .unwrap(); + insert_observation( + &mut conn, + &NewObservation { + session_id: existing_sid, + workspace_id: ws, + project_id: proj, + kind: ObservationKind::UserPrompt, + extension: None, + source_event: None, + title: "before v20".into(), + body: "existing observation survives table rebuild".into(), + importance: 5, + }, + ) + .unwrap(); + } + + let mut conn = Connection::open(&db_path).unwrap(); + conn.pragma_update(None, "foreign_keys", "OFF").unwrap(); + crate::migrations::run_to(&mut conn, 20).unwrap(); + conn.pragma_update(None, "foreign_keys", "ON").unwrap(); + + begin_session( + &mut conn, + &NewSession { + id: SessionId::new(), + workspace_id: ws, + project_id: proj, + agent_kind: AgentKind::Grok, + cwd: None, + }, + ) + .unwrap(); + + let obs_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM observations WHERE session_id = ?1", + params![existing_sid.as_bytes()], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(obs_count, 1, "V20 must preserve existing observations"); + + let index_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master \ + WHERE type = 'index' \ + AND name IN ('idx_sessions_recent', 'idx_sessions_project', 'idx_sessions_started_at')", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(index_count, 3, "V20 must recreate sessions indexes"); + + let trigger_count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master \ + WHERE type = 'trigger' AND name = 'sessions_ws_proj_pairing_ai'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!( + trigger_count, 1, + "V20 must recreate the V18 pairing trigger" + ); + + let other_ws = get_or_create_workspace(&mut conn, "other").unwrap(); + let other_proj = + get_or_create_project(&mut conn, &other_ws, "other-project", None).unwrap(); + let err = begin_session( + &mut conn, + &NewSession { + id: SessionId::new(), + workspace_id: ws, + project_id: other_proj, + agent_kind: AgentKind::Grok, + cwd: None, + }, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("sessions.workspace_id does not match"), + "pairing trigger must reject split-brain sessions after V20: {err}" + ); + + let fk_violations: i64 = conn + .query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |r| { + r.get(0) + }) + .unwrap(); + assert_eq!(fk_violations, 0, "V20 must leave foreign keys clean"); + } + /// V19 is idempotent: re-running on a repaired DB is a no-op. /// Also asserts the initial run on a clean DB (no orphans, no /// empty fragments) is a no-op. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cefd27a4..82d308e9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,8 +7,8 @@ ## Purpose ai-memory is a single Rust binary that gives AI coding agents (Claude -Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, OpenClaw, -OpenCode, OMP, and MCP-capable clients) long-term memory shared across CLIs. +Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, +OpenClaw, OpenCode, OMP, and MCP-capable clients) long-term memory shared across CLIs. Quit one mid-task; open another in the same directory; continue. No manual `write_note` ceremony, no copy-pasting summaries between sessions. @@ -26,7 +26,7 @@ markdown stays the source of truth. ┌──────────────────────┐ │ Claude Code / Codex │ │ Cursor / Gemini CLI │ - │ Antigravity CLI │ + │ Antigravity / Grok │ │ OpenClaw / OpenCode │ │ OMP │ └──────────┬───────────┘ diff --git a/docs/install.md b/docs/install.md index f7aecd1d..a11e2a72 100644 --- a/docs/install.md +++ b/docs/install.md @@ -9,7 +9,7 @@ path (docker + Claude Code). This page covers everything else: - [Arch Linux native packages (AUR)](#arch-linux-native-packages-aur) (systemd system service or user service) - [Configuring other agent CLIs](#configuring-other-agent-clis) - (Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, OpenClaw, VS Code Copilot) + (Codex, OpenCode, OMP, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, Grok Build CLI, OpenClaw, VS Code Copilot) - [Installing hooks without docker](#installing-hooks-without-docker) (curl-based installer) - [Running ai-memory without docker](#running-ai-memory-without-docker) @@ -427,11 +427,11 @@ Each agent CLI needs two things: becomes manual. Claude Desktop is MCP-only today. Claude Code, Codex, OpenCode, OMP, -Cursor, Gemini CLI, Antigravity CLI, and OpenClaw have lifecycle capture paths through +Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, and OpenClaw have lifecycle capture paths through `install-hooks`. > **Two-step hook install pattern.** Claude Code, Codex, Cursor, -> Gemini CLI, and Antigravity CLI use shell/PowerShell hook scripts: (1) `docker cp` the +> Gemini CLI, Antigravity CLI, and Grok Build 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. > On native Windows, Claude Code is the exception to the PowerShell default: @@ -528,7 +528,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, Antigravity CLI, VS Code Copilot +### Cursor, Gemini CLI, Claude Desktop, OpenClaw, Antigravity CLI, Grok Build CLI, VS Code Copilot See [**`docs/mcp-install.md`**](mcp-install.md) for the per-client MCP config file path and snippet, or one-shot it via: @@ -562,6 +562,10 @@ docker run --rm akitaonrails/ai-memory:latest \ install-hooks --agent antigravity-cli --auth-token "$TOKEN" \ --server-url "http://homelab:49374" +docker run --rm akitaonrails/ai-memory:latest \ + install-hooks --agent grok --auth-token "$TOKEN" \ + --server-url "http://homelab:49374" + docker run --rm akitaonrails/ai-memory:latest \ install-mcp --client openclaw --auth-token "$TOKEN" \ --server-url "http://homelab:49374/mcp" @@ -576,12 +580,15 @@ docker run --rm akitaonrails/ai-memory:latest \ ``` Cursor, Gemini CLI, Antigravity CLI, and OpenClaw support both `install-mcp` and -`install-hooks`. Claude Desktop and VS Code Copilot are MCP-only here, +`install-hooks`. Grok Build CLI is hook-only in ai-memory's installer today: +`install-hooks --agent grok` captures lifecycle events, but Grok ignores +`SessionStart` stdout, so handoffs must be accepted through MCP with +`memory_handoff_accept` when resuming. Claude Desktop and VS Code Copilot are 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 -handoff injection at session start or the client's closest equivalent -(Antigravity CLI uses `PreInvocation`). +handoff injection at session start or the client's closest equivalent, except +for Grok's no-stdout SessionStart behavior (Antigravity CLI uses `PreInvocation`). --- @@ -605,7 +612,7 @@ docker run --rm akitaonrails/ai-memory:latest \ ``` The curl script installer supports -`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|opencode|openclaw|omp|pi` +`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|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. diff --git a/docs/mcp-install.md b/docs/mcp-install.md index 3a0775c1..943a72a2 100644 --- a/docs/mcp-install.md +++ b/docs/mcp-install.md @@ -19,13 +19,14 @@ 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, Antigravity CLI, OpenClaw, OpenCode, and +Claude Code, OpenAI Codex, Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, OpenClaw, OpenCode, and OMP have automatic capture integrations (shell/PowerShell hooks for -Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI, TypeScript plugin/extension +Claude Code / Codex / Cursor / Gemini CLI / Antigravity CLI / Grok Build CLI, TypeScript plugin/extension files for OpenClaw / OpenCode / OMP) and are covered in the [main README](../README.md#quick-start). On native Windows, Claude Code uses Git Bash `.sh` hooks rather than the PowerShell default used by other -script-hook agents. +script-hook agents. Grok captures lifecycle events, but it ignores +SessionStart stdout, so ai-memory does not auto-inject handoffs for Grok. Claude Desktop and VS Code Copilot are **MCP-only** here: they expose long-term memory to their LLMs via ai-memory's MCP tools @@ -521,8 +522,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, Cursor, Gemini CLI, OpenClaw, and OMP. Codex, OpenCode, and Antigravity CLI have no true session-end event in the current integration, so ask them 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). | +| **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, Cursor, Gemini CLI, Grok Build CLI, OpenClaw, and OMP. Codex, OpenCode, and Antigravity CLI have no true session-end event in the current integration, so ask them 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. Grok is explicitly excluded because it ignores SessionStart stdout; use (b). (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/docs/windows.md b/docs/windows.md index b5991f30..5bff4125 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -253,7 +253,7 @@ Windows agent builds. Claude Code invokes hooks as a direct binary call (no shell) by default; `AI_MEMORY_HOOK_PLATFORM=windows-bash` restores the Git Bash `bash -c` path. WSL2 Claude Code uses normal WSL `.sh` paths. -- Codex, OpenCode, Cursor, Gemini CLI, and OpenClaw may each choose different +- Codex, OpenCode, Cursor, Gemini CLI, Grok Build CLI, and OpenClaw may each choose different Windows config locations or shell execution behavior. ai-memory uses the current best-known defaults, but they need validation on real installations. diff --git a/hooks/grok/post-tool-use.ps1 b/hooks/grok/post-tool-use.ps1 new file mode 100755 index 00000000..31d7ba81 --- /dev/null +++ b/hooks/grok/post-tool-use.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "post-tool-use" -Agent "grok" +exit 0 diff --git a/hooks/grok/post-tool-use.sh b/hooks/grok/post-tool-use.sh new file mode 100755 index 00000000..a90d381d --- /dev/null +++ b/hooks/grok/post-tool-use.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI post-tool-use hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/pre-compact.ps1 b/hooks/grok/pre-compact.ps1 new file mode 100755 index 00000000..351344cf --- /dev/null +++ b/hooks/grok/pre-compact.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "pre-compact" -Agent "grok" +exit 0 diff --git a/hooks/grok/pre-compact.sh b/hooks/grok/pre-compact.sh new file mode 100755 index 00000000..caae27ba --- /dev/null +++ b/hooks/grok/pre-compact.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI pre-compact hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/pre-tool-use.ps1 b/hooks/grok/pre-tool-use.ps1 new file mode 100755 index 00000000..08b1f9f8 --- /dev/null +++ b/hooks/grok/pre-tool-use.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "pre-tool-use" -Agent "grok" +exit 0 diff --git a/hooks/grok/pre-tool-use.sh b/hooks/grok/pre-tool-use.sh new file mode 100755 index 00000000..9d23f0a0 --- /dev/null +++ b/hooks/grok/pre-tool-use.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI pre-tool-use hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/session-end.ps1 b/hooks/grok/session-end.ps1 new file mode 100755 index 00000000..5564b5e9 --- /dev/null +++ b/hooks/grok/session-end.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "session-end" -Agent "grok" +exit 0 diff --git a/hooks/grok/session-end.sh b/hooks/grok/session-end.sh new file mode 100755 index 00000000..292b84ec --- /dev/null +++ b/hooks/grok/session-end.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI session-end hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/session-start.ps1 b/hooks/grok/session-start.ps1 new file mode 100755 index 00000000..7db4013e --- /dev/null +++ b/hooks/grok/session-start.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "session-start" -Agent "grok" +exit 0 diff --git a/hooks/grok/session-start.sh b/hooks/grok/session-start.sh new file mode 100755 index 00000000..f2c76b39 --- /dev/null +++ b/hooks/grok/session-start.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Grok Build CLI SessionStart hook. +# Grok ignores SessionStart stdout, so this hook captures the event only. +# Do NOT fetch /handoff here: accepting a handoff is destructive and Grok +# would discard the returned context. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/stop.ps1 b/hooks/grok/stop.ps1 new file mode 100755 index 00000000..257aa6bc --- /dev/null +++ b/hooks/grok/stop.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "stop" -Agent "grok" +exit 0 diff --git a/hooks/grok/stop.sh b/hooks/grok/stop.sh new file mode 100755 index 00000000..20a7463d --- /dev/null +++ b/hooks/grok/stop.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI stop hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/hooks/grok/user-prompt-submit.ps1 b/hooks/grok/user-prompt-submit.ps1 new file mode 100755 index 00000000..a05c1bc7 --- /dev/null +++ b/hooks/grok/user-prompt-submit.ps1 @@ -0,0 +1,3 @@ +. "$PSScriptRoot\..\lib\ai-memory-hook.ps1" +Invoke-AiMemoryHook -Event "user-prompt" -Agent "grok" +exit 0 diff --git a/hooks/grok/user-prompt-submit.sh b/hooks/grok/user-prompt-submit.sh new file mode 100755 index 00000000..56cfc07a --- /dev/null +++ b/hooks/grok/user-prompt-submit.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Grok Build CLI user-prompt hook. +_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=grok${QS}" >/dev/null 2>&1 || true +printf '{}\n' +exit 0 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index a2592eaf..d7ee22ed 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|antigravity-cli|opencode|openclaw|omp|pi|oh-my-pi) ;; + claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|opencode|openclaw|omp|pi|oh-my-pi) ;; *) - echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | antigravity-cli | opencode | openclaw | omp | pi | oh-my-pi)" >&2 + echo "unsupported agent: $AGENT (expected claude-code | codex | cursor | gemini-cli | antigravity-cli | grok | opencode | openclaw | omp | pi | oh-my-pi)" >&2 exit 64 ;; esac