Skip to main content

nexus_memory_hooks/
factory.rs

1//! Hook factory for creating agent-specific hooks
2
3use std::collections::HashMap;
4
5use crate::agents::{
6    CLIHook, ClaudeCodeHook, GeminiHook, OhMyPiHook, PiMonoHook, PiSkillsHook, QwenHook,
7};
8use crate::base::AgentHook;
9use crate::error::{HookError, Result};
10use crate::types::AgentType;
11
12/// Factory for creating agent-specific hooks
13///
14/// The factory maintains a registry of supported agent types and
15/// creates the appropriate hook implementation for each.
16///
17/// # Example
18///
19/// ```rust
20/// use nexus_hooks::HookFactory;
21///
22/// fn main() -> Result<(), Box<dyn std::error::Error>> {
23///     let factory = HookFactory::new();
24///
25///     // Create hook for specific agent
26///     let hook = factory.create_hook("claude-code")?;
27///     let hook = factory.create_hook("pi-mono")?;
28///     let hook = factory.create_hook("oh-my-pi")?;
29///
30///     // List supported agents
31///     for agent in factory.supported_agents() {
32///         println!("Supported: {}", agent);
33///     }
34///     Ok(())
35/// }
36/// ```
37pub struct HookFactory {
38    /// Supported agent types
39    supported: HashMap<String, AgentType>,
40
41    /// Aliases for agent types
42    aliases: HashMap<String, String>,
43}
44
45impl HookFactory {
46    /// Create a new hook factory
47    pub fn new() -> Self {
48        let mut supported = HashMap::new();
49        let mut aliases = HashMap::new();
50
51        // Register all supported agent types
52        for agent_type in &[
53            AgentType::ClaudeCode,
54            AgentType::Gemini,
55            AgentType::Qwen,
56            AgentType::PiMono,
57            AgentType::OhMyPi,
58            AgentType::PiSkills,
59            AgentType::OpenCode,
60            AgentType::Codex,
61            AgentType::Amp,
62            AgentType::Droid,
63            AgentType::Generic,
64        ] {
65            supported.insert(agent_type.to_string(), *agent_type);
66        }
67
68        // Register aliases
69        aliases.insert("claude".to_string(), "claude-code".to_string());
70        aliases.insert("pimono".to_string(), "pi-mono".to_string());
71        aliases.insert("pi".to_string(), "pi-mono".to_string());
72        aliases.insert("omp".to_string(), "oh-my-pi".to_string());
73        aliases.insert("ohmypi".to_string(), "oh-my-pi".to_string());
74
75        Self { supported, aliases }
76    }
77
78    /// Create a hook for the specified agent type
79    pub fn create_hook(&self, agent_type: &str) -> Result<Box<dyn AgentHook>> {
80        // Normalize agent type
81        let normalized = self.normalize_agent_type(agent_type);
82
83        // Check if supported
84        let agent_type_enum = self.supported.get(&normalized).copied();
85
86        match agent_type_enum {
87            Some(AgentType::ClaudeCode) => Ok(Box::new(ClaudeCodeHook::new())),
88            Some(AgentType::Gemini) => Ok(Box::new(GeminiHook::new())),
89            Some(AgentType::Qwen) => Ok(Box::new(QwenHook::new())),
90            Some(AgentType::PiMono) => Ok(Box::new(PiMonoHook::new())),
91            Some(AgentType::OhMyPi) => Ok(Box::new(OhMyPiHook::new())),
92            Some(AgentType::PiSkills) => Ok(Box::new(PiSkillsHook::new())),
93            Some(AgentType::OpenCode)
94            | Some(AgentType::Codex)
95            | Some(AgentType::Amp)
96            | Some(AgentType::Droid)
97            | Some(AgentType::Generic) => Ok(Box::new(CLIHook::new(normalized.clone()))),
98            None => Err(HookError::AgentNotFound(format!(
99                "Unknown agent type: {}",
100                agent_type
101            ))),
102        }
103    }
104
105    /// Check if an agent type is supported
106    pub fn is_supported(&self, agent_type: &str) -> bool {
107        let normalized = self.normalize_agent_type(agent_type);
108        self.supported.contains_key(&normalized)
109    }
110
111    /// Get list of supported agent types
112    pub fn supported_agents(&self) -> Vec<String> {
113        self.supported.keys().cloned().collect()
114    }
115
116    /// Get agent type info
117    pub fn get_agent_info(&self, agent_type: &str) -> Option<AgentInfo> {
118        let normalized = self.normalize_agent_type(agent_type);
119        self.supported.get(&normalized).map(|&t| AgentInfo {
120            agent_type: t.to_string(),
121            detection_layer: t.detection_layer(),
122            process_names: t.process_names().iter().map(|s| s.to_string()).collect(),
123            config_dir: t.config_dir().to_string(),
124        })
125    }
126
127    /// Normalize agent type string
128    fn normalize_agent_type(&self, agent_type: &str) -> String {
129        let lower = agent_type.to_lowercase();
130
131        // Check aliases first
132        if let Some(alias) = self.aliases.get(&lower) {
133            alias.clone()
134        } else {
135            lower
136        }
137    }
138
139    /// Register a custom alias
140    pub fn register_alias(&mut self, alias: &str, target: &str) {
141        self.aliases
142            .insert(alias.to_lowercase(), target.to_lowercase());
143    }
144}
145
146impl Default for HookFactory {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152/// Information about an agent type
153#[derive(Debug, Clone)]
154pub struct AgentInfo {
155    pub agent_type: String,
156    pub detection_layer: crate::types::DetectionLayer,
157    pub process_names: Vec<String>,
158    pub config_dir: String,
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_factory_new() {
167        let factory = HookFactory::new();
168        assert!(factory.is_supported("claude-code"));
169        assert!(factory.is_supported("pi-mono"));
170        assert!(factory.is_supported("oh-my-pi"));
171    }
172
173    #[test]
174    fn test_factory_aliases() {
175        let factory = HookFactory::new();
176
177        assert!(factory.is_supported("claude"));
178        assert!(factory.is_supported("pi"));
179        assert!(factory.is_supported("omp"));
180        assert!(factory.is_supported("ohmypi"));
181    }
182
183    #[test]
184    fn test_factory_create_hook() {
185        let factory = HookFactory::new();
186
187        let hook = factory.create_hook("claude-code").unwrap();
188        assert_eq!(hook.agent_type(), "claude-code");
189
190        let hook = factory.create_hook("pi-mono").unwrap();
191        assert_eq!(hook.agent_type(), "pi-mono");
192
193        let hook = factory.create_hook("oh-my-pi").unwrap();
194        assert_eq!(hook.agent_type(), "oh-my-pi");
195    }
196
197    #[test]
198    fn test_factory_create_hook_alias() {
199        let factory = HookFactory::new();
200
201        let hook = factory.create_hook("claude").unwrap();
202        assert_eq!(hook.agent_type(), "claude-code");
203
204        let hook = factory.create_hook("omp").unwrap();
205        assert_eq!(hook.agent_type(), "oh-my-pi");
206    }
207
208    #[test]
209    fn test_factory_unsupported() {
210        let factory = HookFactory::new();
211
212        let result = factory.create_hook("unknown-agent");
213        assert!(result.is_err());
214    }
215
216    #[test]
217    fn test_factory_supported_agents() {
218        let factory = HookFactory::new();
219        let agents = factory.supported_agents();
220
221        assert!(agents.contains(&"claude-code".to_string()));
222        assert!(agents.contains(&"pi-mono".to_string()));
223        assert!(agents.contains(&"oh-my-pi".to_string()));
224    }
225
226    #[test]
227    fn test_factory_get_agent_info() {
228        let factory = HookFactory::new();
229
230        let info = factory.get_agent_info("pi-mono").unwrap();
231        assert_eq!(info.agent_type, "pi-mono");
232        assert_eq!(info.config_dir, ".pi");
233    }
234
235    #[test]
236    fn test_factory_register_alias() {
237        let mut factory = HookFactory::new();
238        factory.register_alias("my-agent", "claude-code");
239
240        assert!(factory.is_supported("my-agent"));
241        let hook = factory.create_hook("my-agent").unwrap();
242        assert_eq!(hook.agent_type(), "claude-code");
243    }
244}