Skip to main content

oparry_wrapper/
claude_wrapper.rs

1//! Claude Code wrapper implementation
2//!
3//! This module provides the main wrapper that intercepts Claude Code
4//! file operations and validates them before writing to disk.
5
6use crate::ipc::IpcChannel;
7use crate::protocol::{ClaudeRequest, ClaudeResponse, IssueDetail};
8use crate::{WrapConfig, ValidatorEngine};
9use oparry_autofix::{AutoFixConfig, AutoFixer, FixStrategy};
10use oparry_core::Result;
11use oparry_parser::Language;
12use std::path::Path;
13use std::sync::Arc;
14use tracing::{debug, info, warn};
15
16/// Claude Code wrapper - intercepts and validates file operations
17pub struct ClaudeWrapper {
18    /// Validation engine
19    validator: Arc<ValidatorEngine>,
20    /// Auto-fix engine
21    autofixer: Option<AutoFixer>,
22    /// Wrapper configuration
23    config: WrapConfig,
24    /// IPC channel
25    ipc: IpcChannel,
26}
27
28impl ClaudeWrapper {
29    /// Create a new Claude wrapper
30    pub fn new(validator: Arc<ValidatorEngine>, config: WrapConfig) -> Self {
31        let autofixer = if config.enable_autofix {
32            Some(AutoFixer::with_config(AutoFixConfig {
33                strategy: match config.autofix_strategy.as_deref() {
34                    Some("safe") => FixStrategy::Safe,
35                    Some("aggressive") => FixStrategy::Aggressive,
36                    _ => FixStrategy::Moderate,
37                },
38                dry_run: config.dry_run,
39                ..Default::default()
40            }))
41        } else {
42            None
43        };
44
45        Self {
46            validator,
47            autofixer,
48            config,
49            ipc: IpcChannel::stdio(),
50        }
51    }
52
53    /// Create wrapper with custom IPC channel
54    pub fn with_ipc(validator: Arc<ValidatorEngine>, config: WrapConfig, ipc: IpcChannel) -> Self {
55        let autofixer = if config.enable_autofix {
56            Some(AutoFixer::with_config(AutoFixConfig {
57                strategy: match config.autofix_strategy.as_deref() {
58                    Some("safe") => FixStrategy::Safe,
59                    Some("aggressive") => FixStrategy::Aggressive,
60                    _ => FixStrategy::Moderate,
61                },
62                dry_run: config.dry_run,
63                ..Default::default()
64            }))
65        } else {
66            None
67        };
68
69        Self {
70            validator,
71            autofixer,
72            config,
73            ipc,
74        }
75    }
76
77    /// Enable or disable auto-fix
78    pub fn with_autofix(mut self, enable: bool) -> Self {
79        if enable && self.autofixer.is_none() {
80            self.autofixer = Some(AutoFixer::new());
81        } else if !enable {
82            self.autofixer = None;
83        }
84        self
85    }
86
87    /// Run the wrapper - enters IPC loop
88    ///
89    /// This blocks and handles requests until stdin is closed
90    pub fn run(&self) -> Result<()> {
91        info!("Starting Claude Code wrapper (v{})", env!("CARGO_PKG_VERSION"));
92
93        let validator = Arc::clone(&self.validator);
94        let config = self.config.clone();
95        let ipc = self.ipc.clone();
96
97        ipc.run_loop(move |request| {
98            Self::handle_request(request, &validator, &config, &None)
99        })
100    }
101
102    /// Handle a single request
103    fn handle_request(
104        request: ClaudeRequest,
105        validator: &ValidatorEngine,
106        config: &WrapConfig,
107        autofixer: &Option<AutoFixer>,
108    ) -> Result<ClaudeResponse> {
109        match request {
110            ClaudeRequest::WriteFile(write_req) => {
111                Self::handle_write_file(write_req, validator, config, autofixer)
112            }
113            ClaudeRequest::EditFile(edit_req) => {
114                Self::handle_edit_file(edit_req, validator, config, autofixer)
115            }
116            ClaudeRequest::Ping => {
117                debug!("Received ping, sending pong");
118                Ok(ClaudeResponse::Pong)
119            }
120        }
121    }
122
123    /// Handle write file request
124    fn handle_write_file(
125        request: crate::protocol::WriteFileRequest,
126        validator: &ValidatorEngine,
127        config: &WrapConfig,
128        autofixer: &Option<AutoFixer>,
129    ) -> Result<ClaudeResponse> {
130        let file_path = &request.path;
131        let content = &request.content;
132        let request_id = &request.id;
133
134        info!("Validating write to: {}", file_path.display());
135
136        // Check path validation first
137        let wrapper = super::StdioWrapper::new(config.clone());
138        if let Err(e) = wrapper.validate_path(file_path) {
139            warn!("Path validation failed: {}", e);
140            return Ok(ClaudeResponse::rejected(
141                request_id,
142                format!("Path not allowed: {}", e),
143                vec![],
144            ));
145        }
146
147        // Detect language from file extension
148        let language = Self::detect_language(file_path);
149        debug!("Detected language: {:?}", language);
150
151        // Run validation
152        let result = validator.validate_string(content, language, file_path);
153
154        if result.passed {
155            info!("Validation passed for: {}", file_path.display());
156            Ok(ClaudeResponse::approved(request_id))
157        } else {
158            warn!("Validation failed for: {}", file_path.display());
159
160            // Convert issues to protocol format
161            let issues: Vec<IssueDetail> = result.issues
162                .clone()
163                .into_iter()
164                .map(|i| i.into())
165                .collect();
166
167            // Check if we have errors (not just warnings)
168            let has_errors = issues.iter()
169                .any(|i| i.level == crate::protocol::IssueSeverity::Error);
170
171            // Try auto-fix if enabled
172            let modified_content = if let Some(fixer) = autofixer {
173                match fixer.fix_issues(content, &result.issues, language, file_path) {
174                    Ok(fix_app) if fix_app.has_changes() => {
175                        info!("Auto-fix applied {} corrections", fix_app.fixes_applied);
176                        Some(fix_app.modified)
177                    }
178                    Ok(_) => None,
179                    Err(e) => {
180                        debug!("Auto-fix failed: {}", e);
181                        None
182                    }
183                }
184            } else {
185                None
186            };
187
188            if has_errors {
189                // If we have a modified content from auto-fix, include it
190                if let Some(modified) = modified_content {
191                    Ok(ClaudeResponse::approved_with_fix(request_id, modified))
192                } else {
193                    Ok(ClaudeResponse::rejected(
194                        request_id,
195                        format!("Validation failed with {} error(s)", issues.len()),
196                        issues,
197                    ))
198                }
199            } else {
200                // Warnings only - allow with warning response
201                Ok(ClaudeResponse::warning(
202                    request_id,
203                    format!("Validation passed with {} warning(s)", issues.len()),
204                    issues,
205                ))
206            }
207        }
208    }
209
210    /// Handle edit file request
211    fn handle_edit_file(
212        request: crate::protocol::EditFileRequest,
213        validator: &ValidatorEngine,
214        config: &WrapConfig,
215        autofixer: &Option<AutoFixer>,
216    ) -> Result<ClaudeResponse> {
217        let file_path = &request.path;
218        let new_content = &request.new_string;
219        let request_id = &request.id;
220
221        info!("Validating edit to: {}", file_path.display());
222
223        // Check path validation first
224        let wrapper = super::StdioWrapper::new(config.clone());
225        if let Err(e) = wrapper.validate_path(file_path) {
226            warn!("Path validation failed: {}", e);
227            return Ok(ClaudeResponse::rejected(
228                request_id,
229                format!("Path not allowed: {}", e),
230                vec![],
231            ));
232        }
233
234        // Detect language from file extension
235        let language = Self::detect_language(file_path);
236        debug!("Detected language: {:?}", language);
237
238        // Run validation on the new content
239        let result = validator.validate_string(new_content, language, file_path);
240
241        if result.passed {
242            info!("Edit validation passed for: {}", file_path.display());
243            Ok(ClaudeResponse::approved(request_id))
244        } else {
245            warn!("Edit validation failed for: {}", file_path.display());
246
247            let issues: Vec<IssueDetail> = result.issues
248                .clone()
249                .into_iter()
250                .map(|i| i.into())
251                .collect();
252
253            let has_errors = issues.iter()
254                .any(|i| i.level == crate::protocol::IssueSeverity::Error);
255
256            // Try auto-fix if enabled
257            let modified_content = if let Some(fixer) = autofixer {
258                match fixer.fix_issues(new_content, &result.issues, language, file_path) {
259                    Ok(fix_app) if fix_app.has_changes() => {
260                        info!("Auto-fix applied {} corrections", fix_app.fixes_applied);
261                        Some(fix_app.modified)
262                    }
263                    Ok(_) => None,
264                    Err(e) => {
265                        debug!("Auto-fix failed: {}", e);
266                        None
267                    }
268                }
269            } else {
270                None
271            };
272
273            if has_errors {
274                // If we have a modified content from auto-fix, include it
275                if let Some(modified) = modified_content {
276                    Ok(ClaudeResponse::approved_with_fix(request_id, modified))
277                } else {
278                    Ok(ClaudeResponse::rejected(
279                        request_id,
280                        format!("Edit validation failed with {} error(s)", issues.len()),
281                        issues,
282                    ))
283                }
284            } else {
285                Ok(ClaudeResponse::warning(
286                    request_id,
287                    format!("Edit validation passed with {} warning(s)", issues.len()),
288                    issues,
289                ))
290            }
291        }
292    }
293
294    /// Detect programming language from file extension
295    fn detect_language(path: &Path) -> Language {
296        Language::from_path(path)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use std::path::PathBuf;
304
305    #[test]
306    fn test_language_detection() {
307        assert_eq!(
308            ClaudeWrapper::detect_language(Path::new("test.ts")),
309            Language::TypeScript
310        );
311        assert_eq!(
312            ClaudeWrapper::detect_language(Path::new("test.js")),
313            Language::JavaScript
314        );
315        assert_eq!(
316            ClaudeWrapper::detect_language(Path::new("test.rs")),
317            Language::Rust
318        );
319    }
320
321    #[test]
322    fn test_ping_request() {
323        // Verify ping returns pong
324        let response = ClaudeWrapper::handle_request(
325            ClaudeRequest::Ping,
326            &ValidatorEngine::new(),
327            &WrapConfig::default(),
328            &None,
329        ).unwrap();
330
331        assert!(matches!(response, ClaudeResponse::Pong));
332    }
333}