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