mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
fix(install-hooks): harden Grok hook support
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Path
|
||||
AgentChoice::Cursor => "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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 │
|
||||
└──────────┬───────────┘
|
||||
|
||||
+15
-8
@@ -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 <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.
|
||||
|
||||
+6
-5
@@ -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:
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "post-tool-use" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "pre-compact" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "pre-tool-use" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "session-end" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "session-start" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+18
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "stop" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "user-prompt" -Agent "grok"
|
||||
exit 0
|
||||
Executable
+15
@@ -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
|
||||
@@ -10,7 +10,7 @@
|
||||
# | bash -s -- --agent claude-code
|
||||
#
|
||||
# Options:
|
||||
# --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>
|
||||
# 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|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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user