repo_description: CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies
repo_topics: agentic-coding ai-coding anthropic claude-code cli command-line-tool cost-reduction developer-tools llm open-source productivity rust token-optimization
code: //! Pattern-matches CLI errors against known correction rules.
  
  use lazy_static::lazy_static;
  use regex::Regex;
  
  #[derive(Debug, Clone, PartialEq)]
  pub enum ErrorType {
      UnknownFlag,
      CommandNotFound,
      #[allow(dead_code)]
      WrongSyntax,
      WrongPath,
      MissingArg,
      PermissionDenied,
      Other(String),
  }
  
  impl ErrorType {
      pub fn as_str(&self) -> &str {
          match self {
              ErrorType::UnknownFlag => "Unknown Flag",
              ErrorType::CommandNotFound => "Command Not Found",
              ErrorType::WrongSyntax => "Wrong Syntax",
              ErrorType::WrongPath => "Wrong Path",
              ErrorType::MissingArg => "Missing Argument",
              ErrorType::PermissionDenied => "Permission Denied",
              ErrorType::Other(s) => s,
          }
      }
  }
  
  #[derive(Debug, Clone)]
  pub struct CorrectionPair {
      pub wrong_command: String,
      pub right_command: String,
      pub error_output: String,
      pub error_type: ErrorType,
      pub confidence: f64,
  }
  
  #[derive(Debug, Clone)]
  pub struct CorrectionRule {
      pub wrong_pattern: String,
      pub right_pattern: String,
      pub error_type: ErrorType,
      pub occurrences: usize,
      pub base_command: String,
      pub example_error: String,
  }
  
  lazy_static! {
      static ref UNKNOWN_FLAG_RE: Regex = Regex::new(
          r"(?i)(unexpected argument|unknown (option|flag)|unrecognized (option|flag)|invalid (option|flag))"
      ).unwrap();
  
      static ref CMD_NOT_FOUND_RE: Regex = Regex::new(
          r"(?i)(command not found|not recognized as an internal|no such file or directory.*command)"
      ).unwrap();
  
      static ref WRONG_PATH_RE: Regex = Regex::new(
          r"(?i)(no such file or directory|cannot find the path|file not found)"
      ).unwrap();
  
      static ref MISSING_ARG_RE: Regex = Regex::new(
          r"(?i)(requires a value|requires an argument|missing (required )?argument|expected.*argument)"
      ).unwrap();
  
      static ref PERMISSION_DENIED_RE: Regex = Regex::new(
          r"(?i)(permission denied|access denied|not permitted)"
      ).unwrap();
  
      // User rejection patterns - NOT actual errors
      static ref USER_REJECTION_RE: Regex = Regex::new(
          r"(?i)(user (doesn't want|declined|rejected|cancelled)|operation (cancelled|aborted) by user)"
      ).unwrap();
  }
  
  /// Filters out user rejections - requires actual error-indicating content
  pub fn is_command_error(is_error: bool, output: &str) -> bool {
      if !is_error {
          return false;
      }
  
      // Reject if it's a user rejection
      if USER_REJECTION_RE.is_match(output) {
          return false;
      }
  
      // Must contain error-indicating content
      let output_lower = output.to_lowercase();
      output_lower.contains("error")
          || output_lower.contains("failed")
          || output_lower.contains("unknown")
          || output_lower.contains("invalid")
          || output_lower.contains("not found")
          || output_lower.contains("permission denied")
          || output_lower.contains("cannot")
  }
  
  pub fn classify_error(output: &str) -> ErrorType {
      if UNKNOWN_FLAG_RE.is_match(output) {
          ErrorType::UnknownFlag
      } else if CMD_NOT_FOUND_RE.is_match(output) {
          ErrorType::CommandNotFound
      } else if MISSING_ARG_RE.is_match(output) {
          ErrorType::MissingArg
      } else if PERMISSION_DENIED_RE.is_match(output) {
          ErrorType::PermissionDenied
      } else if WRONG_PATH_RE.is_match(output) {
          ErrorType::WrongPath
      } else {
          ErrorType::Other("General Error".to_string())
      }
  }
  
  /// Represents a command with its execution result for correction detection
  pub struct CommandExecution {
      pub command: String,
      pub is_error: bool,
      pub output: String,
  }
  
  const CORRECTION_WINDOW: usize = 3;
  const MIN_CONFIDENCE: f64 = 0.6;
  
  /// Extract base command (first 1-2 tokens, stripping env prefixes)
  pub fn extract_base_command(cmd: &str) -> String {
      let trimmed = cmd.trim();
  
      // Strip common env prefixes
      let stripped = trimmed
          .strip_prefix("RUST_BACKTRACE=1 ")
          .or_else(|| trimmed.strip_prefix("NODE_ENV=production "))
          .or_else(|| trimmed.strip_prefix("DEBUG=* "))
          .unwrap_or(trimmed);
  
      // Get first 1-2 tokens
      let parts: Vec<&str> = stripped.split_whitespace().collect();
      match parts.len() {
          0 => String::new(),
          1 => parts[0].to_string(),
          _ => format!("{} {}", parts[0], parts[1]),
      }
  }
  
  /// Calculate similarity between two commands using Jaccard similarity
  /// Same base command = 0.5 base score + up to 0.5 from argument similarity
  pub fn command_similarity(a: &str, b: &str) -> f64 {
      let base_a = extract_base_command(a);
      let base_b = extract_base_command(b);
  
      if base_a != base_b {
          return 0.0;
      }
  
      // Extract args (everything after base command)
      let args_a: std::collections::HashSet<&str> = a
          .strip_prefix(&base_a)
          .unwrap_or("")
          .split_whitespace()
          .collect();
  
      let args_b: std::collections::HashSet<&str> = b
          .strip_prefix(&base_b)
          .unwrap_or("")
          .split_whitespace()
          .collect();
  
      if args_a.is_empty() && args_b.is_empty() {
          return 1.0; // Identical commands
      }
  
      let intersection = args_a.intersection(&args_b).count();
      let union = args_a.union(&args_b).count();
  
      if union == 0 {
          return 0.5; // Same base, no args
      }
  
      // 0.5 for same base + up to 0.5 for arg similarity
      0.5 + (intersection as f64 / union as f64) * 0.5
  }
  
  /// Check if error is a compilation/test error (TDD cycle, not CLI correction)
  fn is_tdd_cycle_error(error_type: &ErrorType, output: &str) -> bool {
      // Compilation errors
      if output.contains("error[E") || output.contains("aborting due to") {
          return true;
      }
  
      // Test failures
      if output.contains("test result: FAILED") || output.contains("tests failed") {
          return true;
      }
  
      // Only syntax errors are CLI corrections
      matches!(error_type, ErrorType::CommandNotFound | ErrorType::Other(_))
          && (output.contains("error[E") || output.contains("FAILED"))
  }
  
  /// Check if commands differ only by path (exploration, not correction)
  fn differs_only_by_path(a: &str, b: &str) -> bool {
      let base_a = extract_base_command(a);
      let base_b = extract_base_command(b);
  
      if base_a != base_b {
          return false;
      }
  
      // Simple heuristic: if similarity is very high (>0.9) but not identical,
      // likely just path differences
      let sim = command_similarity(a, b);
      sim > 0.9 && sim < 1.0
  }
  
  pub fn find_corrections(commands: &[CommandExecution]) -> Vec<CorrectionPair> {
      let mut corrections = Vec::new();
  
      for i in 0..commands.len() {
          let cmd = &commands[i];
  
          // Must be an actual error
          if !is_command_error(cmd.is_error, &cmd.output) {
              continue;
          }
  
          let error_type = classify_error(&cmd.output);
  
          // Skip TDD cycle errors
          if is_tdd_cycle_error(&error_type, &cmd.output) {
              continue;
          }
  
          // Look ahead for correction within CORRECTION_WINDOW
          for candidate in commands.iter().skip(i + 1).take(CORRECTION_WINDOW) {
              let similarity = command_similarity(&cmd.command, &candidate.command);
  
              // Must meet minimum similarity
              if similarity < 0.5 {
                  continue;
              }
  
              // Skip if only path differs (exploration)
              if differs_only_by_path(&cmd.command, &candidate.command) {
                  continue;
              }
  
              // Skip if identical commands (same error repeated)
              if cmd.command == candidate.command {
                  continue;
              }
  
              // Calculate confidence
              let mut confidence = similarity;
  
              // Boost confidence if correction succeeded
              if !is_command_error(candidate.is_error, &candidate.output) {
                  confidence = (confidence + 0.2).min(1.0);
              }
  
              // Must meet minimum confidence
              if confidence < MIN_CONFIDENCE {
                  continue;
              }
  
              // Found a correction!
              corrections.push(CorrectionPair {
                  wrong_command: cmd.command.clone(),
                  right_command: candidate.command.clone(),
                  error_output: cmd.output.chars().take(500).collect(),
                  error_type: error_type.clone(),
                  confidence,
              });
  
              // Take first match only
              break;
          }
      }
  
      corrections
  }
  
  /// Extract the specific token that changed between wrong and right commands
  fn extract_diff_token(wrong: &str, right: &str) -> String {
      let wrong_parts: std::collections::HashSet<&str> = wrong.split_whitespace().collect();
      let right_parts: std::collections::HashSet<&str> = right.split_whitespace().collect();
  
      // Find tokens in wrong but not in right (removed)
      let removed: Vec<&str> = wrong_parts.difference(&right_parts).copied().collect();
  
      // Find tokens in right but not in wrong (added)
      let added: Vec<&str> = right_parts.difference(&wrong_parts).copied().collect();
  
      // Return the most distinctive change
      if !removed.is_empty() && !added.is_empty() {
          format!("{} → {}", removed[0], added[0])
      } else if !removed.is_empty() {
          format!("removed {}", removed[0])
      } else if !added.is_empty() {
          format!("added {}", added[0])
      } else {
          "unknown".to_string()
      }
  }
  
  pub fn deduplicate_corrections(pairs: Vec<CorrectionPair>) -> Vec<CorrectionRule> {
      use std::collections::HashMap;
  
      let mut groups: HashMap<(String, String, String), Vec<CorrectionPair>> = HashMap::new();
  
      // Group by (base_command, error_type, diff_token)
      for pair in pairs {
          let base = extract_base_command(&pair.wrong_command);
          let error_type_str = pair.error_type.as_str().to_string();
          let diff_token = extract_diff_token(&pair.wrong_command, &pair.right_command);
  
          let key = (base, error_type_str, diff_token);
          groups.entry(key).or_default().push(pair);
      }
  
      // For each group, keep the best confidence example
      let mut rules = Vec::new();
      for ((base_command, _error_type_str, _diff_token), mut group) in groups {
          // Sort by confidence descending
          group.sort_by(|a, b| {
              b.confidence
                  .partial_cmp(&a.confidence)
                  .unwrap_or(std::cmp::Ordering::Equal)
          });
  
          let best = &group[0];
          let occurrences = group.len();
  
          // Reconstruct ErrorType from string (simplified - just use first one)
          let error_type = best.error_type.clone();
  
          rules.push(CorrectionRule {
              wrong_pattern: best.wrong_command.clone(),
              right_pattern: best.right_command.clone(),
              error_type,
              occurrences,
              base_command,
              example_error: best.error_output.clone(),
          });
      }
  
      // Sort by occurrences descending (most common mistakes first)
      rules.sort_by(|a, b| b.occurrences.cmp(&a.occurrences));
  
      rules
  }
  
  #[cfg(test)]
  mod tests {
      use super::*;
  
      #[test]
      fn test_is_command_error_requires_error_flag() {
          assert!(!is_command_error(false, "error: unknown flag"));
          assert!(is_command_error(true, "error: unknown flag"));
      }
  
      #[test]
      fn test_is_command_error_filters_user_rejection() {
          assert!(!is_command_error(true, "The user doesn't want to proceed"));
          assert!(!is_command_error(true, "Operation cancelled by user"));
          assert!(is_command_error(true, "error: permission denied"));
      }
  
      #[test]
      fn test_is_command_error_requires_error_content() {
          assert!(!is_command_error(true, "All good, success!"));
          assert!(is_command_error(true, "error: something failed"));
          assert!(is_command_error(true, "unknown flag --foo"));
          assert!(is_command_error(true, "invalid option"));
      }
  
      #[test]
      fn test_classify_error_unknown_flag() {
          assert_eq!(
              classify_error("error: unexpected argument '--foo'"),
              ErrorType::UnknownFlag
          );
          assert_eq!(
              classify_error("unknown option: --bar"),
              ErrorType::UnknownFlag
          );
          assert_eq!(
              classify_error("unrecognized flag: -x"),
              ErrorType::UnknownFlag
          );
      }
  
      #[test]
      fn test_classify_error_command_not_found() {
          assert_eq!(
              classify_error("bash: foobar: command not found"),
              ErrorType::CommandNotFound
          );
          assert_eq!(
              classify_error("'xyz' is not recognized as an internal or external command"),
              ErrorType::CommandNotFound
          );
      }
  
      #[test]
      fn test_classify_error_all_types() {
          assert_eq!(
              classify_error("No such file or directory: foo.txt"),
              ErrorType::WrongPath
          );
          assert_eq!(
              classify_error("error: --output requires a value"),
              ErrorType::MissingArg
          );
          assert_eq!(
              classify_error("permission denied: /etc/shadow"),
              ErrorType::PermissionDenied
          );
          assert!(matches!(
              classify_error("something went wrong"),
              ErrorType::Other(_)
          ));
      }
  
      #[test]
      fn test_extract_base_command() {
          assert_eq!(extract_base_command("git commit"), "git commit");
          assert_eq!(extract_base_command("cargo test"), "cargo test");
          assert_eq!(
              extract_base_command("git commit --amend -m 'fix'"),
              "git commit"
          );
          assert_eq!(
              extract_base_command("RUST_BACKTRACE=1 cargo test"),
              "cargo test"
          );
      }
  
      #[test]
      fn test_command_similarity_same_base() {
          assert_eq!(command_similarity("git commit", "git commit"), 1.0);
          assert_eq!(command_similarity("git status", "npm install"), 0.0);
          let sim = command_similarity("git commit --amend", "git commit --ammend");
          // Debug: check what similarity actually is
          println!("Similarity: {}", sim);
          // Same base (0.5) + both have 1 arg, 0 intersection = 0.5 + 0 = 0.5
          assert_eq!(sim, 0.5);
      }
  
      #[test]
      fn test_find_corrections_basic() {
          let commands = vec![
              CommandExecution {
                  command: "git commit --ammend".to_string(),
                  is_error: true,
                  output: "error: unexpected argument '--ammend'".to_string(),
              },
              CommandExecution {
                  command: "git commit --amend".to_string(),
                  is_error: false,
                  output: "[main abc123] Fix bug".to_string(),
              },
          ];
  
          let corrections = find_corrections(&commands);
          assert_eq!(corrections.len(), 1);
          assert_eq!(corrections[0].wrong_command, "git commit --ammend");
          assert_eq!(corrections[0].right_command, "git commit --amend");
          assert!(corrections[0].confidence >= 0.6);
      }
  
      #[test]
      fn test_find_corrections_window_limit() {
          let commands = vec![
              CommandExecution {
                  command: "git commit --ammend".to_string(),
                  is_error: true,
                  output: "error: unexpected argument '--ammend'".to_string(),
              },
              CommandExecution {
                  command: "ls".to_string(),
                  is_error: false,
                  output: "file1.txt\nfile2.txt".to_string(),
              },
              CommandExecution {
                  command: "pwd".to_string(),
                  is_error: false,
                  output: "/home/user".to_string(),
              },
              CommandExecution {
                  command: "echo test".to_string(),
                  is_error: false,
                  output: "test".to_string(),
              },
              // Outside CORRECTION_WINDOW (3)
              CommandExecution {
                  command: "git commit --amend".to_string(),
                  is_error: false,
                  output: "[main abc123] Fix".to_string(),
              },
          ];
  
          let corrections = find_corrections(&commands);
          assert_eq!(corrections.len(), 0); // Too far apart
      }
  
      #[test]
      fn test_find_corrections_excludes_tdd_cycle() {
          let commands = vec![
              CommandExecution {
                  command: "cargo test".to_string(),
                  is_error: true,
                  output: "error[E0425]: cannot find value `x`\ntest result: FAILED".to_string(),
              },
              CommandExecution {
                  command: "cargo test".to_string(),
                  is_error: false,
                  output: "test result: ok. 5 passed".to_string(),
              },
          ];
  
          let corrections = find_corrections(&commands);
          assert_eq!(corrections.len(), 0); // TDD cycle, not CLI correction
      }
  
      #[test]
      fn test_find_corrections_path_exploration() {
          let commands = vec![
              CommandExecution {
                  command: "cat file1.txt".to_string(),
                  is_error: true,
                  output: "cat: file1.txt: No such file or directory".to_string(),
              },
              CommandExecution {
                  command: "cat file2.txt".to_string(),
                  is_error: false,
                  output: "content here".to_string(),
              },
          ];
  
          let corrections = find_corrections(&commands);
          // Should be filtered as path exploration (differs_only_by_path)
          // Actually, this should NOT be filtered since base commands differ enough
          // Let me adjust: they have same base "cat" but different args
          assert_eq!(corrections.len(), 0); // Different files = exploration
      }
  
      #[test]
      fn test_find_corrections_min_confidence() {
          let commands = vec![
              CommandExecution {
                  command: "git commit --foo --bar --baz".to_string(),
                  is_error: true,
                  output: "error: unexpected argument '--foo'".to_string(),
              },
              CommandExecution {
                  command: "git commit --qux".to_string(),
                  is_error: false,
                  output: "[main abc123] Fix".to_string(),
              },
          ];
  
          let corrections = find_corrections(&commands);
          // Similarity = 0.5 (same base) + 0 (no arg overlap) = 0.5
          // With success boost: 0.5 + 0.2 = 0.7, which passes MIN_CONFIDENCE
          // So we expect 1 correction (this is a valid correction despite different args)
          assert_eq!(corrections.len(), 1);
      }
  
      #[test]
      fn test_deduplicate_corrections_merges_same() {
          let pairs = vec![
              CorrectionPair {
                  wrong_command: "git commit --ammend".to_string(),
                  right_command: "git commit --amend".to_string(),
                  error_output: "error: unexpected argument '--ammend'".to_string(),
                  error_type: ErrorType::UnknownFlag,
                  confidence: 0.8,
              },
              CorrectionPair {
                  wrong_command: "git commit --ammend -m 'fix'".to_string(),
                  right_command: "git commit --amend -m 'fix'".to_string(),
                  error_output: "error: unexpected argument '--ammend'".to_string(),
                  error_type: ErrorType::UnknownFlag,
                  confidence: 0.9,
              },
              CorrectionPair {
                  wrong_command: "git commit --ammend".to_string(),
                  right_command: "git commit --amend".to_string(),
                  error_output: "error: unexpected argument '--ammend'".to_string(),
                  error_type: ErrorType::UnknownFlag,
                  confidence: 0.7,
              },
          ];
  
          let rules = deduplicate_corrections(pairs);
          assert_eq!(rules.len(), 1); // Merged into single rule
          assert_eq!(rules[0].occurrences, 3);
          assert_eq!(rules[0].base_command, "git commit");
          // Should keep highest confidence example (0.9)
          assert!(rules[0].wrong_pattern.contains("'fix'"));
      }
  
      #[test]
      fn test_deduplicate_corrections_keeps_distinct() {
          let pairs = vec![
              CorrectionPair {
                  wrong_command: "git commit --ammend".to_string(),
                  right_command: "git commit --amend".to_string(),
                  error_output: "error: unexpected argument '--ammend'".to_string(),
                  error_type: ErrorType::UnknownFlag,
                  confidence: 0.8,
              },
              CorrectionPair {
                  wrong_command: "git push --force".to_string(),
                  right_command: "git push --force-with-lease".to_string(),
                  error_output: "error: --force is dangerous".to_string(),
                  error_type: ErrorType::WrongSyntax,
                  confidence: 0.7,
              },
          ];
  
          let rules = deduplicate_corrections(pairs);
          assert_eq!(rules.len(), 2); // Different base commands and errors
          assert_eq!(rules[0].occurrences, 1);
          assert_eq!(rules[1].occurrences, 1);
      }
  }
  