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, DroidHook, GeminiHook, OhMyPiHook, PiMonoHook, PiSkillsHook, QwenHook,
7};
8use crate::base::AgentHook;
9use crate::error::{HookError, Result};
10use crate::types::{AgentType, SupportTier};
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_memory_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::Hermes,
64            AgentType::Generic,
65        ] {
66            supported.insert(agent_type.to_string(), *agent_type);
67        }
68
69        // Register aliases
70        aliases.insert("claude".to_string(), "claude-code".to_string());
71        aliases.insert("pimono".to_string(), "pi-mono".to_string());
72        aliases.insert("pi".to_string(), "pi-mono".to_string());
73        aliases.insert("omp".to_string(), "oh-my-pi".to_string());
74        aliases.insert("ohmypi".to_string(), "oh-my-pi".to_string());
75        aliases.insert("factory".to_string(), "droid".to_string());
76        aliases.insert("factory-cli".to_string(), "droid".to_string());
77
78        Self { supported, aliases }
79    }
80
81    /// Create a hook for the specified agent type
82    pub fn create_hook(&self, agent_type: &str) -> Result<Box<dyn AgentHook>> {
83        self.create_hook_internal(agent_type, false)
84    }
85
86    /// Create a hook for inspection/status reporting without mutating user state.
87    pub fn create_hook_readonly(&self, agent_type: &str) -> Result<Box<dyn AgentHook>> {
88        self.create_hook_internal(agent_type, true)
89    }
90
91    fn create_hook_internal(&self, agent_type: &str, readonly: bool) -> Result<Box<dyn AgentHook>> {
92        // Normalize agent type
93        let normalized = self.normalize_agent_type(agent_type);
94
95        // Check if supported
96        let agent_type_enum = self.supported.get(&normalized).copied();
97
98        match agent_type_enum {
99            Some(AgentType::ClaudeCode) => Ok(Box::new(ClaudeCodeHook::new())),
100            Some(AgentType::Gemini) => Ok(Box::new(GeminiHook::new())),
101            Some(AgentType::Qwen) => Ok(Box::new(QwenHook::new())),
102            Some(AgentType::PiMono) => Ok(Box::new(if readonly {
103                PiMonoHook::new_readonly()
104            } else {
105                PiMonoHook::new()
106            })),
107            Some(AgentType::OhMyPi) => Ok(Box::new(if readonly {
108                OhMyPiHook::new_readonly()
109            } else {
110                OhMyPiHook::new()
111            })),
112            Some(AgentType::PiSkills) => Ok(Box::new(if readonly {
113                PiSkillsHook::new_readonly()
114            } else {
115                PiSkillsHook::new()
116            })),
117            Some(AgentType::OpenCode)
118            | Some(AgentType::Codex)
119            | Some(AgentType::Amp)
120            | Some(AgentType::Hermes)
121            | Some(AgentType::Generic) => Ok(Box::new(CLIHook::new(normalized.clone()))),
122            Some(AgentType::Droid) => Ok(Box::new(if readonly {
123                DroidHook::new_readonly()
124            } else {
125                DroidHook::new()
126            })),
127            None => Err(HookError::AgentNotFound(format!(
128                "Unknown agent type: {}",
129                agent_type
130            ))),
131        }
132    }
133
134    /// Check if an agent type is supported
135    pub fn is_supported(&self, agent_type: &str) -> bool {
136        let normalized = self.normalize_agent_type(agent_type);
137        self.supported.contains_key(&normalized)
138    }
139
140    /// Get list of supported agent types
141    pub fn supported_agents(&self) -> Vec<String> {
142        self.supported.keys().cloned().collect()
143    }
144
145    /// Get agent type info
146    pub fn get_agent_info(&self, agent_type: &str) -> Option<AgentInfo> {
147        let normalized = self.normalize_agent_type(agent_type);
148        self.supported.get(&normalized).map(|&t| AgentInfo {
149            agent_type: t.to_string(),
150            detection_layer: t.detection_layer(),
151            support_tier: t.support_tier(),
152            process_names: t.process_names().iter().map(|s| s.to_string()).collect(),
153            config_dir: t.config_dir().to_string(),
154        })
155    }
156
157    /// Normalize agent type string
158    fn normalize_agent_type(&self, agent_type: &str) -> String {
159        let lower = agent_type.to_lowercase();
160
161        // Check aliases first
162        if let Some(alias) = self.aliases.get(&lower) {
163            alias.clone()
164        } else {
165            lower
166        }
167    }
168
169    /// Register a custom alias
170    pub fn register_alias(&mut self, alias: &str, target: &str) {
171        self.aliases
172            .insert(alias.to_lowercase(), target.to_lowercase());
173    }
174}
175
176impl Default for HookFactory {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Information about an agent type
183#[derive(Debug, Clone)]
184pub struct AgentInfo {
185    pub agent_type: String,
186    pub detection_layer: crate::types::DetectionLayer,
187    pub support_tier: SupportTier,
188    pub process_names: Vec<String>,
189    pub config_dir: String,
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_factory_new() {
198        let factory = HookFactory::new();
199        assert!(factory.is_supported("claude-code"));
200        assert!(factory.is_supported("pi-mono"));
201        assert!(factory.is_supported("oh-my-pi"));
202    }
203
204    #[test]
205    fn test_factory_aliases() {
206        let factory = HookFactory::new();
207
208        assert!(factory.is_supported("claude"));
209        assert!(factory.is_supported("pi"));
210        assert!(factory.is_supported("omp"));
211        assert!(factory.is_supported("ohmypi"));
212        assert!(factory.is_supported("factory"));
213        assert!(factory.is_supported("factory-cli"));
214    }
215
216    #[test]
217    fn test_factory_create_hook() {
218        let factory = HookFactory::new();
219
220        let hook = factory.create_hook("claude-code").unwrap();
221        assert_eq!(hook.agent_type(), "claude-code");
222
223        let hook = factory.create_hook("pi-mono").unwrap();
224        assert_eq!(hook.agent_type(), "pi-mono");
225
226        let hook = factory.create_hook("oh-my-pi").unwrap();
227        assert_eq!(hook.agent_type(), "oh-my-pi");
228    }
229
230    #[test]
231    fn test_factory_create_hook_alias() {
232        let factory = HookFactory::new();
233
234        let hook = factory.create_hook("claude").unwrap();
235        assert_eq!(hook.agent_type(), "claude-code");
236
237        let hook = factory.create_hook("omp").unwrap();
238        assert_eq!(hook.agent_type(), "oh-my-pi");
239    }
240
241    #[test]
242    fn test_factory_unsupported() {
243        let factory = HookFactory::new();
244
245        let result = factory.create_hook("unknown-agent");
246        assert!(result.is_err());
247    }
248
249    #[test]
250    fn test_factory_supported_agents() {
251        let factory = HookFactory::new();
252        let agents = factory.supported_agents();
253
254        assert!(agents.contains(&"claude-code".to_string()));
255        assert!(agents.contains(&"pi-mono".to_string()));
256        assert!(agents.contains(&"oh-my-pi".to_string()));
257        assert!(agents.contains(&"hermes".to_string()));
258    }
259
260    #[test]
261    fn test_factory_get_agent_info() {
262        let factory = HookFactory::new();
263
264        let info = factory.get_agent_info("pi-mono").unwrap();
265        assert_eq!(info.agent_type, "pi-mono");
266        assert_eq!(info.config_dir, ".pi");
267    }
268
269    #[test]
270    fn test_factory_register_alias() {
271        let mut factory = HookFactory::new();
272        factory.register_alias("my-agent", "claude-code");
273
274        assert!(factory.is_supported("my-agent"));
275        let hook = factory.create_hook("my-agent").unwrap();
276        assert_eq!(hook.agent_type(), "claude-code");
277    }
278}