Skip to main content

agent_hooks/
gemini.rs

1//! Gemini CLI adapter.
2//!
3//! Registers hooks in `~/.gemini/hooks.json`.
4
5use std::path::{Path, PathBuf};
6
7use serde_json::{json, Value};
8
9use crate::detection;
10use crate::{AdapterError, Result, ToolAdapter};
11
12const HOOKS_REL: &str = ".gemini/hooks.json";
13
14const EVENTS: &[&str] = &[
15    "turn_complete",
16    "user_prompt_submit",
17];
18
19pub struct GeminiAdapter;
20
21impl GeminiAdapter {
22    pub fn new() -> Self {
23        Self
24    }
25
26    fn hooks_path(&self) -> Option<PathBuf> {
27        detection::home_path(HOOKS_REL)
28    }
29}
30
31impl ToolAdapter for GeminiAdapter {
32    fn name(&self) -> &str {
33        "gemini"
34    }
35
36    fn display_name(&self) -> &str {
37        "Gemini CLI"
38    }
39
40    fn is_installed(&self) -> bool {
41        detection::home_dir_exists(".gemini") || detection::command_exists("gemini")
42    }
43
44    fn hooks_registered(&self) -> bool {
45        let Some(path) = self.hooks_path() else {
46            return false;
47        };
48        let Ok(content) = std::fs::read_to_string(&path) else {
49            return false;
50        };
51        content.contains("hook_event_bridge")
52    }
53
54    fn register_hooks(&self, bridge_script: &Path) -> Result<()> {
55        let path = self.hooks_path().ok_or(AdapterError::NoHomeDir)?;
56        detection::ensure_parent_dir(&path)?;
57
58        let mut root: Value = if path.exists() {
59            let content = std::fs::read_to_string(&path)?;
60            serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
61        } else {
62            json!({})
63        };
64
65        let obj = root
66            .as_object_mut()
67            .ok_or_else(|| AdapterError::Config("Invalid hooks.json".into()))?;
68
69        let cmd = bridge_script.to_string_lossy().to_string();
70
71        for event in EVENTS {
72            let arr = obj.entry(*event).or_insert_with(|| json!([]));
73            if !arr.is_array() {
74                *arr = json!([]);
75            }
76            let hooks = arr.as_array_mut().unwrap();
77
78            let already = hooks.iter().any(|h| {
79                h.get("command")
80                    .and_then(|c| c.as_str())
81                    .is_some_and(|c| c == cmd)
82            });
83
84            if !already {
85                hooks.push(json!({ "command": cmd }));
86            }
87        }
88
89        detection::backup_file(&path);
90        let output = serde_json::to_string_pretty(&root)?;
91        std::fs::write(&path, output)?;
92        Ok(())
93    }
94
95    fn unregister_hooks(&self) -> Result<()> {
96        let path = self.hooks_path().ok_or(AdapterError::NoHomeDir)?;
97        if !path.exists() {
98            return Ok(());
99        }
100
101        let content = std::fs::read_to_string(&path)?;
102        let mut root: Value = serde_json::from_str(&content)?;
103
104        let Some(obj) = root.as_object_mut() else {
105            return Ok(());
106        };
107
108        let mut modified = false;
109        for event in EVENTS {
110            if let Some(arr) = obj.get_mut(*event).and_then(|v| v.as_array_mut()) {
111                let before = arr.len();
112                arr.retain(|h| {
113                    !h.get("command")
114                        .and_then(|c| c.as_str())
115                        .is_some_and(|c| c.contains("hook_event_bridge"))
116                });
117                if arr.len() != before {
118                    modified = true;
119                }
120            }
121        }
122
123        if modified {
124            detection::backup_file(&path);
125            let output = serde_json::to_string_pretty(&root)?;
126            std::fs::write(&path, output)?;
127        }
128        Ok(())
129    }
130
131    fn config_path(&self) -> Option<PathBuf> {
132        self.hooks_path()
133    }
134
135    fn supported_events(&self) -> &[&str] {
136        EVENTS
137    }
138}