cmdai/models/
mod.rs

1// Models module - Core data structures
2// These are placeholder stubs - tests should fail until proper implementation
3
4use serde::{Deserialize, Serialize};
5
6/// Request for command generation from natural language
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CommandRequest {
9    /// Natural language description of desired command
10    pub input: String,
11
12    /// Target shell type for command generation
13    pub shell: ShellType,
14
15    /// User's safety preference level
16    pub safety_level: SafetyLevel,
17
18    /// Optional additional context (current directory, environment info)
19    pub context: Option<String>,
20
21    /// Optional backend preference
22    pub backend_preference: Option<String>,
23}
24
25impl CommandRequest {
26    /// Create a new command request with the given input and shell type
27    pub fn new(input: impl Into<String>, shell: ShellType) -> Self {
28        let input = input.into();
29        let trimmed = input.trim().to_string();
30
31        Self {
32            input: trimmed,
33            shell,
34            safety_level: SafetyLevel::default(),
35            context: None,
36            backend_preference: None,
37        }
38    }
39
40    /// Set the safety level (builder pattern)
41    pub fn with_safety(mut self, level: SafetyLevel) -> Self {
42        self.safety_level = level;
43        self
44    }
45
46    /// Set the context (builder pattern)
47    pub fn with_context(mut self, ctx: impl Into<String>) -> Self {
48        self.context = Some(ctx.into());
49        self
50    }
51
52    /// Set the backend preference (builder pattern)
53    pub fn with_backend(mut self, backend: impl Into<String>) -> Self {
54        self.backend_preference = Some(backend.into());
55        self
56    }
57
58    /// Validate that the request is well-formed
59    pub fn validate(&self) -> Result<(), String> {
60        if self.input.is_empty() {
61            return Err("Input cannot be empty".to_string());
62        }
63        Ok(())
64    }
65}
66
67/// Response from command generation with metadata
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct GeneratedCommand {
70    /// The generated shell command
71    pub command: String,
72
73    /// Human-readable explanation of what the command does
74    pub explanation: String,
75
76    /// Assessed risk level of the command
77    pub safety_level: RiskLevel,
78
79    /// Description of estimated impact
80    pub estimated_impact: String,
81
82    /// Alternative commands that could achieve similar results
83    pub alternatives: Vec<String>,
84
85    /// Name of the backend that generated this command
86    pub backend_used: String,
87
88    /// Time taken to generate the command in milliseconds
89    pub generation_time_ms: u64,
90
91    /// Confidence score (0.0 to 1.0)
92    pub confidence_score: f64,
93}
94
95impl GeneratedCommand {
96    /// Validate that the generated command is well-formed
97    pub fn validate(&self) -> Result<(), String> {
98        if self.command.is_empty() {
99            return Err("Command cannot be empty".to_string());
100        }
101        if self.explanation.is_empty() {
102            return Err("Explanation cannot be empty".to_string());
103        }
104        if !(0.0..=1.0).contains(&self.confidence_score) {
105            return Err(format!(
106                "Confidence score must be between 0.0 and 1.0, got {}",
107                self.confidence_score
108            ));
109        }
110        Ok(())
111    }
112}
113
114impl std::fmt::Display for GeneratedCommand {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        use colored::Colorize;
117
118        writeln!(f, "{}", "Generated Command:".bold())?;
119        writeln!(f, "  {}", self.command.bright_cyan().bold())?;
120        writeln!(f)?;
121        writeln!(f, "{}", "Explanation:".bold())?;
122        writeln!(f, "  {}", self.explanation)?;
123        writeln!(f)?;
124        writeln!(f, "{} {}", "Risk Level:".bold(), self.safety_level)?;
125        writeln!(f, "{} {}", "Backend:".bold(), self.backend_used)?;
126        writeln!(
127            f,
128            "{} {:.0}%",
129            "Confidence:".bold(),
130            self.confidence_score * 100.0
131        )?;
132
133        if !self.alternatives.is_empty() {
134            writeln!(f)?;
135            writeln!(f, "{}", "Alternatives:".bold())?;
136            for alt in &self.alternatives {
137                writeln!(f, "  • {}", alt.dimmed())?;
138            }
139        }
140
141        Ok(())
142    }
143}
144
145#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
146#[serde(rename_all = "lowercase")]
147pub enum RiskLevel {
148    Safe,
149    Moderate,
150    High,
151    Critical,
152}
153
154impl RiskLevel {
155    /// Check if this risk level requires user confirmation at the given safety level
156    pub fn requires_confirmation(&self, safety_level: SafetyLevel) -> bool {
157        match safety_level {
158            SafetyLevel::Strict => matches!(self, Self::Moderate | Self::High | Self::Critical),
159            SafetyLevel::Moderate => matches!(self, Self::High | Self::Critical),
160            SafetyLevel::Permissive => matches!(self, Self::Critical),
161        }
162    }
163
164    /// Check if this risk level should be blocked at the given safety level
165    pub fn is_blocked(&self, safety_level: SafetyLevel) -> bool {
166        match safety_level {
167            SafetyLevel::Strict => matches!(self, Self::High | Self::Critical),
168            SafetyLevel::Moderate => matches!(self, Self::Critical),
169            SafetyLevel::Permissive => false,
170        }
171    }
172}
173
174impl std::fmt::Display for RiskLevel {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        use colored::Colorize;
177        match self {
178            Self::Safe => write!(f, "{}", "Safe".green()),
179            Self::Moderate => write!(f, "{}", "Moderate".yellow()),
180            Self::High => write!(f, "{}", "High".bright_red()),
181            Self::Critical => write!(f, "{}", "Critical".red().bold()),
182        }
183    }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "lowercase")]
188pub enum SafetyLevel {
189    /// Blocks High and Critical commands, confirms Moderate
190    Strict,
191    /// Blocks Critical commands, confirms High
192    Moderate,
193    /// Warns about all dangerous commands but allows with confirmation
194    Permissive,
195}
196
197impl std::str::FromStr for SafetyLevel {
198    type Err = String;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        match s.to_lowercase().as_str() {
202            "strict" => Ok(SafetyLevel::Strict),
203            "moderate" => Ok(SafetyLevel::Moderate),
204            "permissive" => Ok(SafetyLevel::Permissive),
205            _ => Err(format!(
206                "Invalid safety level '{}'. Valid values: strict, moderate, permissive",
207                s
208            )),
209        }
210    }
211}
212
213impl Default for SafetyLevel {
214    fn default() -> Self {
215        Self::Moderate
216    }
217}
218
219impl std::fmt::Display for SafetyLevel {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Self::Strict => write!(f, "strict"),
223            Self::Moderate => write!(f, "moderate"),
224            Self::Permissive => write!(f, "permissive"),
225        }
226    }
227}
228
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "lowercase")]
231pub enum BackendType {
232    /// Mock backend for testing
233    Mock,
234    /// Embedded model backend (Qwen with MLX/CPU)
235    Embedded,
236    /// Ollama local LLM backend
237    Ollama,
238    /// vLLM HTTP API backend
239    VLlm,
240    /// Apple Silicon MLX backend (legacy)
241    Mlx,
242}
243
244impl std::str::FromStr for BackendType {
245    type Err = String;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        match s.to_lowercase().as_str() {
249            "mock" => Ok(Self::Mock),
250            "embedded" => Ok(Self::Embedded),
251            "ollama" => Ok(Self::Ollama),
252            "vllm" => Ok(Self::VLlm),
253            "mlx" => Ok(Self::Mlx),
254            _ => Err(format!("Unknown backend type: {}", s)),
255        }
256    }
257}
258
259impl std::fmt::Display for BackendType {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        match self {
262            Self::Mock => write!(f, "mock"),
263            Self::Embedded => write!(f, "embedded"),
264            Self::Ollama => write!(f, "ollama"),
265            Self::VLlm => write!(f, "vllm"),
266            Self::Mlx => write!(f, "mlx"),
267        }
268    }
269}
270
271#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "lowercase")]
273pub enum ShellType {
274    Bash,
275    Zsh,
276    Fish,
277    Sh,
278    PowerShell,
279    Cmd,
280    Unknown,
281}
282
283impl ShellType {
284    /// Detect the current shell from environment
285    pub fn detect() -> Self {
286        // Check SHELL environment variable on Unix-like systems
287        if let Ok(shell) = std::env::var("SHELL") {
288            if shell.contains("bash") {
289                return Self::Bash;
290            } else if shell.contains("zsh") {
291                return Self::Zsh;
292            } else if shell.contains("fish") {
293                return Self::Fish;
294            } else if shell.ends_with("/sh") {
295                return Self::Sh;
296            }
297        }
298
299        // Check for Windows shells
300        #[cfg(target_os = "windows")]
301        {
302            if std::env::var("PSModulePath").is_ok() {
303                return Self::PowerShell;
304            }
305            return Self::Cmd;
306        }
307
308        Self::Unknown
309    }
310
311    /// Check if this is a POSIX-compatible shell
312    pub fn is_posix(&self) -> bool {
313        matches!(self, Self::Bash | Self::Zsh | Self::Fish | Self::Sh)
314    }
315
316    /// Check if this is a Windows shell
317    pub fn is_windows(&self) -> bool {
318        matches!(self, Self::PowerShell | Self::Cmd)
319    }
320}
321
322impl Default for ShellType {
323    fn default() -> Self {
324        Self::detect()
325    }
326}
327
328impl std::str::FromStr for ShellType {
329    type Err = String;
330
331    fn from_str(s: &str) -> Result<Self, Self::Err> {
332        match s.to_lowercase().as_str() {
333            "bash" => Ok(Self::Bash),
334            "zsh" => Ok(Self::Zsh),
335            "fish" => Ok(Self::Fish),
336            "sh" => Ok(Self::Sh),
337            "powershell" | "pwsh" => Ok(Self::PowerShell),
338            "cmd" => Ok(Self::Cmd),
339            _ => Ok(Self::Unknown),
340        }
341    }
342}
343
344impl std::fmt::Display for ShellType {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        match self {
347            Self::Bash => write!(f, "bash"),
348            Self::Zsh => write!(f, "zsh"),
349            Self::Fish => write!(f, "fish"),
350            Self::Sh => write!(f, "sh"),
351            Self::PowerShell => write!(f, "powershell"),
352            Self::Cmd => write!(f, "cmd"),
353            Self::Unknown => write!(f, "unknown"),
354        }
355    }
356}
357
358/// Backend metadata for diagnostics and selection
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct BackendInfo {
361    /// Type of backend
362    pub backend_type: BackendType,
363
364    /// Name of the model being used
365    pub model_name: String,
366
367    /// Whether this backend supports streaming responses
368    pub supports_streaming: bool,
369
370    /// Maximum number of tokens the model can generate
371    pub max_tokens: u32,
372
373    /// Typical latency in milliseconds
374    pub typical_latency_ms: u64,
375
376    /// Memory usage in megabytes
377    pub memory_usage_mb: u64,
378
379    /// Backend version string
380    pub version: String,
381}
382
383impl BackendInfo {
384    /// Validate that backend info has reasonable values
385    pub fn validate(&self) -> Result<(), String> {
386        if self.model_name.is_empty() {
387            return Err("Model name cannot be empty".to_string());
388        }
389        if self.max_tokens == 0 {
390            return Err("Max tokens must be positive".to_string());
391        }
392        if self.version.is_empty() {
393            return Err("Version cannot be empty".to_string());
394        }
395        Ok(())
396    }
397}
398
399// All types are public through mod.rs exports
400
401// ============================================================================
402// Infrastructure Models (Feature 003)
403// ============================================================================
404
405use chrono::{DateTime, Utc};
406use std::collections::HashMap;
407use std::path::PathBuf;
408
409/// Platform operating system type
410#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
411#[serde(rename_all = "lowercase")]
412pub enum Platform {
413    Linux,
414    MacOS,
415    Windows,
416}
417
418impl Platform {
419    /// Detect current platform at runtime
420    pub fn detect() -> Self {
421        #[cfg(target_os = "linux")]
422        return Platform::Linux;
423
424        #[cfg(target_os = "macos")]
425        return Platform::MacOS;
426
427        #[cfg(target_os = "windows")]
428        return Platform::Windows;
429
430        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
431        compile_error!("Unsupported platform");
432    }
433
434    /// Check if platform is POSIX-compliant
435    pub fn is_posix(&self) -> bool {
436        matches!(self, Platform::Linux | Platform::MacOS)
437    }
438}
439
440impl std::fmt::Display for Platform {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        match self {
443            Platform::Linux => write!(f, "Linux"),
444            Platform::MacOS => write!(f, "macOS"),
445            Platform::Windows => write!(f, "Windows"),
446        }
447    }
448}
449
450/// Log severity level
451#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
452#[serde(rename_all = "lowercase")]
453pub enum LogLevel {
454    Debug,
455    Info,
456    Warn,
457    Error,
458}
459
460impl LogLevel {
461    /// Convert to tracing Level
462    pub fn to_tracing_level(&self) -> tracing::Level {
463        match self {
464            LogLevel::Debug => tracing::Level::DEBUG,
465            LogLevel::Info => tracing::Level::INFO,
466            LogLevel::Warn => tracing::Level::WARN,
467            LogLevel::Error => tracing::Level::ERROR,
468        }
469    }
470}
471
472impl std::str::FromStr for LogLevel {
473    type Err = String;
474
475    fn from_str(s: &str) -> Result<Self, Self::Err> {
476        match s.to_lowercase().as_str() {
477            "debug" => Ok(LogLevel::Debug),
478            "info" => Ok(LogLevel::Info),
479            "warn" | "warning" => Ok(LogLevel::Warn),
480            "error" | "err" => Ok(LogLevel::Error),
481            _ => Err(format!(
482                "Invalid log level '{}'. Valid options: debug, info, warn, error",
483                s
484            )),
485        }
486    }
487}
488
489impl std::fmt::Display for LogLevel {
490    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491        match self {
492            LogLevel::Debug => write!(f, "DEBUG"),
493            LogLevel::Info => write!(f, "INFO"),
494            LogLevel::Warn => write!(f, "WARN"),
495            LogLevel::Error => write!(f, "ERROR"),
496        }
497    }
498}
499
500/// Cached model metadata
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct CachedModel {
503    pub model_id: String,
504    pub path: PathBuf,
505    pub checksum: String,
506    pub size_bytes: u64,
507    pub downloaded_at: DateTime<Utc>,
508    pub last_accessed: DateTime<Utc>,
509    pub version: Option<String>,
510}
511
512impl CachedModel {
513    /// Validate cached model metadata
514    pub fn validate(&self) -> Result<(), String> {
515        if self.model_id.is_empty() {
516            return Err("Model ID cannot be empty".to_string());
517        }
518        if self.checksum.len() != 64 {
519            return Err(format!(
520                "Checksum must be 64 characters (SHA256 hex), got {}",
521                self.checksum.len()
522            ));
523        }
524        if !self.checksum.chars().all(|c| c.is_ascii_hexdigit()) {
525            return Err("Checksum must be valid hexadecimal".to_string());
526        }
527        Ok(())
528    }
529}
530
531/// Cache manifest tracking all cached models
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct CacheManifest {
534    pub version: String,
535    pub models: HashMap<String, CachedModel>,
536    pub total_size_bytes: u64,
537    pub max_cache_size_bytes: u64,
538    pub last_updated: DateTime<Utc>,
539}
540
541impl CacheManifest {
542    /// Create a new empty manifest
543    pub fn new(max_size_gb: u64) -> Self {
544        Self {
545            version: "1.0.0".to_string(),
546            models: HashMap::new(),
547            total_size_bytes: 0,
548            max_cache_size_bytes: max_size_gb * 1024 * 1024 * 1024,
549            last_updated: Utc::now(),
550        }
551    }
552
553    /// Add a model to the manifest
554    pub fn add_model(&mut self, model: CachedModel) {
555        self.total_size_bytes += model.size_bytes;
556        self.models.insert(model.model_id.clone(), model);
557        self.last_updated = Utc::now();
558    }
559
560    /// Remove a model from the manifest
561    pub fn remove_model(&mut self, model_id: &str) -> Option<CachedModel> {
562        if let Some(model) = self.models.remove(model_id) {
563            self.total_size_bytes = self.total_size_bytes.saturating_sub(model.size_bytes);
564            self.last_updated = Utc::now();
565            Some(model)
566        } else {
567            None
568        }
569    }
570
571    /// Get a model from the manifest
572    pub fn get_model(&self, model_id: &str) -> Option<&CachedModel> {
573        self.models.get(model_id)
574    }
575
576    /// Clean up least-recently-used models if over size limit
577    pub fn cleanup_lru(&mut self) -> Vec<String> {
578        let mut removed = Vec::new();
579
580        while self.total_size_bytes > self.max_cache_size_bytes && !self.models.is_empty() {
581            // Find LRU model
582            let lru_model_id = self
583                .models
584                .iter()
585                .min_by_key(|(_, model)| model.last_accessed)
586                .map(|(id, _)| id.clone());
587
588            if let Some(model_id) = lru_model_id {
589                self.remove_model(&model_id);
590                removed.push(model_id);
591            } else {
592                break;
593            }
594        }
595
596        removed
597    }
598
599    /// Validate integrity of all models
600    pub fn validate_integrity(&self) -> (Vec<String>, Vec<String>, Vec<String>) {
601        let mut valid = Vec::new();
602        let mut corrupted = Vec::new();
603        let mut missing = Vec::new();
604
605        for (model_id, model) in &self.models {
606            if !model.path.exists() {
607                missing.push(model_id.clone());
608            } else if model.validate().is_err() {
609                corrupted.push(model_id.clone());
610            } else {
611                valid.push(model_id.clone());
612            }
613        }
614
615        (valid, corrupted, missing)
616    }
617}
618
619/// User configuration with preferences
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct UserConfiguration {
622    pub default_shell: Option<ShellType>,
623    pub safety_level: SafetyLevel,
624    pub default_model: Option<String>,
625    pub log_level: LogLevel,
626    pub cache_max_size_gb: u64,
627    pub log_rotation_days: u32,
628}
629
630impl Default for UserConfiguration {
631    fn default() -> Self {
632        Self {
633            default_shell: None, // Auto-detect
634            safety_level: SafetyLevel::Moderate,
635            default_model: None,
636            log_level: LogLevel::Info,
637            cache_max_size_gb: 10,
638            log_rotation_days: 7,
639        }
640    }
641}
642
643impl UserConfiguration {
644    /// Create a builder for UserConfiguration
645    pub fn builder() -> UserConfigurationBuilder {
646        UserConfigurationBuilder::new()
647    }
648
649    /// Validate configuration values
650    pub fn validate(&self) -> Result<(), String> {
651        if self.cache_max_size_gb < 1 || self.cache_max_size_gb > 1000 {
652            return Err(format!(
653                "cache_max_size_gb must be between 1 and 1000, got {}",
654                self.cache_max_size_gb
655            ));
656        }
657        if self.log_rotation_days < 1 || self.log_rotation_days > 365 {
658            return Err(format!(
659                "log_rotation_days must be between 1 and 365, got {}",
660                self.log_rotation_days
661            ));
662        }
663        Ok(())
664    }
665}
666
667/// Builder for UserConfiguration
668pub struct UserConfigurationBuilder {
669    default_shell: Option<ShellType>,
670    safety_level: SafetyLevel,
671    default_model: Option<String>,
672    log_level: LogLevel,
673    cache_max_size_gb: u64,
674    log_rotation_days: u32,
675}
676
677impl Default for UserConfigurationBuilder {
678    fn default() -> Self {
679        Self::new()
680    }
681}
682
683impl UserConfigurationBuilder {
684    pub fn new() -> Self {
685        let defaults = UserConfiguration::default();
686        Self {
687            default_shell: defaults.default_shell,
688            safety_level: defaults.safety_level,
689            default_model: defaults.default_model,
690            log_level: defaults.log_level,
691            cache_max_size_gb: defaults.cache_max_size_gb,
692            log_rotation_days: defaults.log_rotation_days,
693        }
694    }
695
696    pub fn default_shell(mut self, shell: ShellType) -> Self {
697        self.default_shell = Some(shell);
698        self
699    }
700
701    pub fn safety_level(mut self, level: SafetyLevel) -> Self {
702        self.safety_level = level;
703        self
704    }
705
706    pub fn default_model(mut self, model: impl Into<String>) -> Self {
707        self.default_model = Some(model.into());
708        self
709    }
710
711    pub fn log_level(mut self, level: LogLevel) -> Self {
712        self.log_level = level;
713        self
714    }
715
716    pub fn cache_max_size_gb(mut self, size: u64) -> Self {
717        self.cache_max_size_gb = size;
718        self
719    }
720
721    pub fn log_rotation_days(mut self, days: u32) -> Self {
722        self.log_rotation_days = days;
723        self
724    }
725
726    pub fn build(self) -> Result<UserConfiguration, String> {
727        let config = UserConfiguration {
728            default_shell: self.default_shell,
729            safety_level: self.safety_level,
730            default_model: self.default_model,
731            log_level: self.log_level,
732            cache_max_size_gb: self.cache_max_size_gb,
733            log_rotation_days: self.log_rotation_days,
734        };
735        config.validate()?;
736        Ok(config)
737    }
738}
739
740/// Configuration schema for validation
741pub struct ConfigSchema {
742    pub known_sections: Vec<String>,
743    pub known_keys: HashMap<String, String>,
744    pub deprecated_keys: HashMap<String, String>,
745}
746
747impl ConfigSchema {
748    pub fn new() -> Self {
749        let mut known_keys = HashMap::new();
750        known_keys.insert(
751            "general.safety_level".to_string(),
752            "SafetyLevel enum".to_string(),
753        );
754        known_keys.insert(
755            "general.default_shell".to_string(),
756            "ShellType enum".to_string(),
757        );
758        known_keys.insert("general.default_model".to_string(), "String".to_string());
759        known_keys.insert("logging.log_level".to_string(), "LogLevel enum".to_string());
760        known_keys.insert("logging.log_rotation_days".to_string(), "u32".to_string());
761        known_keys.insert("cache.max_size_gb".to_string(), "u64".to_string());
762
763        Self {
764            known_sections: vec![
765                "general".to_string(),
766                "logging".to_string(),
767                "cache".to_string(),
768            ],
769            known_keys,
770            deprecated_keys: HashMap::new(),
771        }
772    }
773
774    pub fn validate(&self, _config: &UserConfiguration) -> Result<(), String> {
775        // Validation is done in UserConfiguration::validate()
776        Ok(())
777    }
778}
779
780/// Execution context captured at runtime
781#[derive(Debug, Clone, Serialize, Deserialize)]
782pub struct ExecutionContext {
783    pub current_dir: PathBuf,
784    pub shell_type: ShellType,
785    pub platform: Platform,
786    pub environment_vars: HashMap<String, String>,
787    pub username: String,
788    pub hostname: String,
789    pub captured_at: DateTime<Utc>,
790}
791
792impl ExecutionContext {
793    /// Create new execution context with custom values
794    pub fn new(
795        current_dir: PathBuf,
796        shell_type: ShellType,
797        platform: Platform,
798    ) -> Result<Self, String> {
799        if !current_dir.is_absolute() {
800            return Err("Current directory must be absolute path".to_string());
801        }
802
803        // Capture and filter environment variables
804        let environment_vars = Self::filter_env_vars();
805
806        Ok(Self {
807            current_dir,
808            shell_type,
809            platform,
810            environment_vars,
811            username: std::env::var("USER")
812                .or_else(|_| std::env::var("USERNAME"))
813                .unwrap_or_else(|_| "unknown".to_string()),
814            hostname: std::env::var("HOSTNAME")
815                .or_else(|_| std::env::var("COMPUTERNAME"))
816                .unwrap_or_else(|_| "unknown".to_string()),
817            captured_at: Utc::now(),
818        })
819    }
820
821    /// Filter environment variables to exclude sensitive data
822    fn filter_env_vars() -> HashMap<String, String> {
823        let sensitive_patterns = [
824            "API_KEY",
825            "TOKEN",
826            "SECRET",
827            "PASSWORD",
828            "PASSWD",
829            "CREDENTIAL",
830            "AUTH",
831            "PRIVATE",
832            "KEY",
833        ];
834
835        std::env::vars()
836            .filter(|(key, value)| {
837                // Filter out sensitive variables and empty values
838                !key.is_empty()
839                    && !value.is_empty()
840                    && !sensitive_patterns
841                        .iter()
842                        .any(|pattern| key.to_uppercase().contains(pattern))
843            })
844            .collect()
845    }
846
847    /// Serialize context for LLM prompt
848    pub fn to_prompt_context(&self) -> String {
849        format!(
850            "Current directory: {}\nShell: {}\nPlatform: {}\nUser: {}@{}",
851            self.current_dir.display(),
852            self.shell_type,
853            self.platform,
854            self.username,
855            self.hostname
856        )
857    }
858
859    /// Check if environment variable exists
860    pub fn has_env_var(&self, key: &str) -> bool {
861        self.environment_vars.contains_key(key)
862    }
863
864    /// Get environment variable value
865    pub fn get_env_var(&self, key: &str) -> Option<&str> {
866        self.environment_vars.get(key).map(|s| s.as_str())
867    }
868}
869
870/// Structured log entry
871#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct LogEntry {
873    pub timestamp: DateTime<Utc>,
874    pub level: LogLevel,
875    pub target: String,
876    pub message: String,
877    pub operation_id: Option<String>,
878    pub metadata: HashMap<String, serde_json::Value>,
879    pub duration_ms: Option<u64>,
880}
881
882impl LogEntry {
883    /// Create a new log entry
884    pub fn new(level: LogLevel, target: impl Into<String>, message: impl Into<String>) -> Self {
885        Self {
886            timestamp: Utc::now(),
887            level,
888            target: target.into(),
889            message: message.into(),
890            operation_id: None,
891            metadata: HashMap::new(),
892            duration_ms: None,
893        }
894    }
895
896    /// Add metadata field
897    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
898        self.metadata.insert(key.into(), value);
899        self
900    }
901
902    /// Set operation ID
903    pub fn with_operation_id(mut self, id: impl Into<String>) -> Self {
904        self.operation_id = Some(id.into());
905        self
906    }
907
908    /// Set duration
909    pub fn with_duration(mut self, duration_ms: u64) -> Self {
910        self.duration_ms = Some(duration_ms);
911        self
912    }
913}