1use 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
16pub struct ClaudeWrapper {
18 validator: Arc<ValidatorEngine>,
20 autofixer: Option<AutoFixer>,
22 config: WrapConfig,
24 ipc: IpcChannel,
26}
27
28impl ClaudeWrapper {
29 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 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 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 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 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 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 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 let language = Self::detect_language(file_path);
149 debug!("Detected language: {:?}", language);
150
151 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 let issues: Vec<IssueDetail> = result.issues
162 .clone()
163 .into_iter()
164 .map(|i| i.into())
165 .collect();
166
167 let has_errors = issues.iter()
169 .any(|i| i.level == crate::protocol::IssueSeverity::Error);
170
171 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 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 Ok(ClaudeResponse::warning(
202 request_id,
203 format!("Validation passed with {} warning(s)", issues.len()),
204 issues,
205 ))
206 }
207 }
208 }
209
210 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 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 let language = Self::detect_language(file_path);
236 debug!("Detected language: {:?}", language);
237
238 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 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 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 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 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}