Skip to main content

agent_hooks/
cursor.rs

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