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