1use std::path::{Path, PathBuf};
7
8use serde_json::{json, Value};
9
10use crate::detection;
11use crate::{AdapterError, Result, ToolAdapter};
12
13const SETTINGS_REL: &str = ".claude/settings.json";
14
15const EVENTS: &[&str] = &[
17 "Stop",
18 "Notification",
19 "UserPromptSubmit",
20 "SubagentStart",
21 "PreCompact",
22];
23
24pub struct ClaudeAdapter;
25
26impl ClaudeAdapter {
27 pub fn new() -> Self {
28 Self
29 }
30
31 fn settings_path(&self) -> Option<PathBuf> {
32 detection::home_path(SETTINGS_REL)
33 }
34
35 fn register_event(&self, settings_path: &Path, bridge_cmd: &str, event: &str) -> Result<()> {
37 detection::ensure_parent_dir(settings_path)?;
38
39 let mut root: Value = if settings_path.exists() {
40 let content = std::fs::read_to_string(settings_path)?;
41 serde_json::from_str(&content)?
42 } else {
43 json!({})
44 };
45
46 let obj = root
47 .as_object_mut()
48 .ok_or_else(|| AdapterError::Config("Invalid root JSON".into()))?;
49
50 let hooks = obj.entry("hooks").or_insert_with(|| json!({}));
51 if !hooks.is_object() {
52 *hooks = json!({});
53 }
54 let hooks_obj = hooks
55 .as_object_mut()
56 .ok_or_else(|| AdapterError::Config("Invalid hooks format".into()))?;
57
58 let event_hooks = hooks_obj.entry(event).or_insert_with(|| json!([]));
59 if !event_hooks.is_array() {
60 *event_hooks = json!([]);
61 }
62 let arr = event_hooks
63 .as_array_mut()
64 .ok_or_else(|| AdapterError::Config("Invalid event hooks format".into()))?;
65
66 let already = arr.iter().any(|item| {
68 item.get("hooks")
69 .and_then(|h| h.as_array())
70 .map(|a| {
71 a.iter().any(|h| {
72 h.get("command")
73 .and_then(|c| c.as_str())
74 .is_some_and(|c| c == bridge_cmd)
75 })
76 })
77 .unwrap_or(false)
78 });
79
80 if !already {
81 arr.push(json!({
82 "matcher": "",
83 "hooks": [{
84 "type": "command",
85 "command": bridge_cmd
86 }]
87 }));
88
89 detection::backup_file(settings_path);
90 let output = serde_json::to_string_pretty(&root)?;
91 std::fs::write(settings_path, output)?;
92 }
93
94 Ok(())
95 }
96
97 fn unregister_event(
99 &self,
100 settings_path: &Path,
101 bridge_cmd: &str,
102 event: &str,
103 ) -> Result<()> {
104 if !settings_path.exists() {
105 return Ok(());
106 }
107
108 let content = std::fs::read_to_string(settings_path)?;
109 let mut root: Value = serde_json::from_str(&content)?;
110
111 let modified = if let Some(hooks) = root
112 .as_object_mut()
113 .and_then(|o| o.get_mut("hooks"))
114 .and_then(|h| h.as_object_mut())
115 {
116 if let Some(arr) = hooks.get_mut(event).and_then(|v| v.as_array_mut()) {
117 let before = arr.len();
118 arr.retain(|item| {
119 !item
120 .get("hooks")
121 .and_then(|h| h.as_array())
122 .map(|a| {
123 a.iter().any(|h| {
124 h.get("command")
125 .and_then(|c| c.as_str())
126 .is_some_and(|c| c == bridge_cmd)
127 })
128 })
129 .unwrap_or(false)
130 });
131 arr.len() != before
132 } else {
133 false
134 }
135 } else {
136 false
137 };
138
139 if modified {
140 detection::backup_file(settings_path);
141 let output = serde_json::to_string_pretty(&root)?;
142 std::fs::write(settings_path, output)?;
143 }
144
145 Ok(())
146 }
147
148 fn has_bridge_hook(&self, settings_path: &Path) -> bool {
150 let Ok(content) = std::fs::read_to_string(settings_path) else {
151 return false;
152 };
153 let Ok(root) = serde_json::from_str::<Value>(&content) else {
154 return false;
155 };
156
157 let Some(hooks) = root.get("hooks").and_then(|h| h.as_object()) else {
158 return false;
159 };
160
161 for event in EVENTS {
163 if let Some(arr) = hooks.get(*event).and_then(|v| v.as_array()) {
164 for item in arr {
165 if let Some(inner) = item.get("hooks").and_then(|h| h.as_array()) {
166 for h in inner {
167 if let Some(cmd) = h.get("command").and_then(|c| c.as_str()) {
168 if cmd.contains("hook_event_bridge") {
169 return true;
170 }
171 }
172 }
173 }
174 }
175 }
176 }
177
178 false
179 }
180}
181
182impl ToolAdapter for ClaudeAdapter {
183 fn name(&self) -> &str {
184 "claude"
185 }
186
187 fn display_name(&self) -> &str {
188 "Claude Code"
189 }
190
191 fn is_installed(&self) -> bool {
192 detection::home_dir_exists(".claude") || detection::command_exists("claude")
193 }
194
195 fn hooks_registered(&self) -> bool {
196 self.settings_path()
197 .map(|p| self.has_bridge_hook(&p))
198 .unwrap_or(false)
199 }
200
201 fn register_hooks(&self, bridge_script: &Path) -> Result<()> {
202 let settings = self
203 .settings_path()
204 .ok_or(AdapterError::NoHomeDir)?;
205 let cmd = bridge_script.to_string_lossy().to_string();
206
207 for event in EVENTS {
208 self.register_event(&settings, &cmd, event)?;
209 }
210 Ok(())
211 }
212
213 fn unregister_hooks(&self) -> Result<()> {
214 let settings = self
215 .settings_path()
216 .ok_or(AdapterError::NoHomeDir)?;
217
218 let cmd = detection::home_path(".agent-hand/hooks/hook_event_bridge.sh")
220 .map(|p| p.to_string_lossy().to_string())
221 .unwrap_or_default();
222
223 if cmd.is_empty() {
224 return Ok(());
225 }
226
227 for event in EVENTS {
228 self.unregister_event(&settings, &cmd, event)?;
229 }
230 Ok(())
231 }
232
233 fn config_path(&self) -> Option<PathBuf> {
234 self.settings_path()
235 }
236
237 fn supported_events(&self) -> &[&str] {
238 EVENTS
239 }
240}