#[derive(Serialize)] struct FallbackErrorOutput { error: FallbackError, } #[derive(Serialize)] struct FallbackError { code: String, message: String, } fn handle_error(e: Box, robot_mode: bool) -> ! { if let Some(gi_error) = e.downcast_ref::() { if robot_mode { let output = RobotErrorOutput::from(gi_error); eprintln!( "{}", serde_json::to_string(&output).unwrap_or_else(|_| { let fallback = FallbackErrorOutput { error: FallbackError { code: "INTERNAL_ERROR".to_string(), message: gi_error.to_string(), }, }; serde_json::to_string(&fallback) .unwrap_or_else(|_| r#"{"error":{"code":"INTERNAL_ERROR","message":"Serialization failed"}}"#.to_string()) }) ); std::process::exit(gi_error.exit_code()); } else { eprintln!(); eprintln!( " {} {}", Theme::error().render(Icons::error()), Theme::error().bold().render(&gi_error.to_string()) ); if let Some(suggestion) = gi_error.suggestion() { eprintln!(); eprintln!(" {suggestion}"); } let actions = gi_error.actions(); if !actions.is_empty() { eprintln!(); for action in &actions { eprintln!( " {} {}", Theme::dim().render("\u{2192}"), Theme::bold().render(action) ); } } eprintln!(); std::process::exit(gi_error.exit_code()); } } if robot_mode { let output = FallbackErrorOutput { error: FallbackError { code: "INTERNAL_ERROR".to_string(), message: e.to_string(), }, }; eprintln!( "{}", serde_json::to_string(&output).unwrap_or_else(|_| { r#"{"error":{"code":"INTERNAL_ERROR","message":"Serialization failed"}}"# .to_string() }) ); } else { eprintln!(); eprintln!( " {} {}", Theme::error().render(Icons::error()), Theme::error().bold().render(&e.to_string()) ); eprintln!(); } std::process::exit(1); } /// Emit stderr warnings for any corrections applied during Phase 1.5. fn emit_correction_warnings(result: &CorrectionResult, robot_mode: bool) { if robot_mode { #[derive(Serialize)] struct CorrectionWarning<'a> { warning: CorrectionWarningInner<'a>, } #[derive(Serialize)] struct CorrectionWarningInner<'a> { r#type: &'static str, corrections: &'a [autocorrect::Correction], teaching: Vec, } let teaching: Vec = result .corrections .iter() .map(autocorrect::format_teaching_note) .collect(); let warning = CorrectionWarning { warning: CorrectionWarningInner { r#type: "ARG_CORRECTED", corrections: &result.corrections, teaching, }, }; if let Ok(json) = serde_json::to_string(&warning) { eprintln!("{json}"); } } else { for c in &result.corrections { eprintln!( "{} {}", Theme::warning().render("Auto-corrected:"), autocorrect::format_teaching_note(c) ); } } } /// Phase 1 & 4: Handle clap parsing errors with structured JSON output in robot mode. /// Also includes fuzzy command matching and flag-level suggestions. fn handle_clap_error(e: clap::Error, robot_mode: bool, corrections: &CorrectionResult) -> ! { use clap::error::ErrorKind; // Always let clap handle --help and --version normally (print and exit 0). // These are intentional user actions, not errors, even when stdout is redirected. if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) { e.exit() } if robot_mode { let error_code = map_clap_error_kind(e.kind()); let full_msg = e.to_string(); let message = full_msg .lines() .take(3) .collect::>() .join("; ") .trim() .to_string(); let (suggestion, correction, valid_values) = match e.kind() { // Phase 4: Suggest similar command for unknown subcommands ErrorKind::InvalidSubcommand => { let suggestion = if let Some(invalid_cmd) = extract_invalid_subcommand(&e) { suggest_similar_command(&invalid_cmd) } else { "Run 'lore robot-docs' for valid commands".to_string() }; (suggestion, None, None) } // Flag-level fuzzy matching for unknown flags ErrorKind::UnknownArgument => { let invalid_flag = extract_invalid_flag(&e); let similar = invalid_flag .as_deref() .and_then(|flag| autocorrect::suggest_similar_flag(flag, &corrections.args)); let suggestion = if let Some(ref s) = similar { format!("Did you mean '{s}'? Run 'lore robot-docs' for all flags") } else { "Run 'lore robot-docs' for valid flags".to_string() }; (suggestion, similar, None) } // Value-level suggestions for invalid enum values ErrorKind::InvalidValue => { let (flag, valid_vals) = extract_invalid_value_context(&e); let suggestion = if let Some(vals) = &valid_vals { format!( "Valid values: {}. Run 'lore robot-docs' for details", vals.join(", ") ) } else if let Some(ref f) = flag { if let Some(vals) = autocorrect::valid_values_for_flag(f) { format!("Valid values for {f}: {}", vals.join(", ")) } else { "Run 'lore robot-docs' for valid values".to_string() } } else { "Run 'lore robot-docs' for valid values".to_string() }; let vals_vec = valid_vals.or_else(|| { flag.as_deref() .and_then(autocorrect::valid_values_for_flag) .map(|v| v.iter().map(|s| (*s).to_string()).collect()) }); (suggestion, None, vals_vec) } ErrorKind::MissingRequiredArgument => { let suggestion = format!( "A required argument is missing. {}", if let Some(subcmd) = extract_subcommand_from_context(&e) { format!( "Example: {}. Run 'lore {subcmd} --help' for required arguments", command_example(&subcmd) ) } else { "Run 'lore robot-docs' for command reference".to_string() } ); (suggestion, None, None) } ErrorKind::MissingSubcommand => { let suggestion = "No command specified. Common commands: issues, mrs, search, sync, \ timeline, who, me. Run 'lore robot-docs' for the full list" .to_string(); (suggestion, None, None) } ErrorKind::TooFewValues | ErrorKind::TooManyValues => { let suggestion = if let Some(subcmd) = extract_subcommand_from_context(&e) { format!( "Example: {}. Run 'lore {subcmd} --help' for usage", command_example(&subcmd) ) } else { "Run 'lore robot-docs' for command reference".to_string() }; (suggestion, None, None) } _ => ( "Run 'lore robot-docs' for valid commands".to_string(), None, None, ), }; let output = RobotErrorWithSuggestion { error: RobotErrorSuggestionData { code: error_code.to_string(), message, suggestion, correction, valid_values, }, }; eprintln!( "{}", serde_json::to_string(&output).unwrap_or_else(|_| { r#"{"error":{"code":"PARSE_ERROR","message":"Parse error"}}"#.to_string() }) ); std::process::exit(2); } else { e.exit() } } /// Map clap ErrorKind to semantic error codes fn map_clap_error_kind(kind: clap::error::ErrorKind) -> &'static str { use clap::error::ErrorKind; match kind { ErrorKind::InvalidSubcommand => "UNKNOWN_COMMAND", ErrorKind::UnknownArgument => "UNKNOWN_FLAG", ErrorKind::MissingRequiredArgument => "MISSING_REQUIRED", ErrorKind::InvalidValue => "INVALID_VALUE", ErrorKind::ValueValidation => "INVALID_VALUE", ErrorKind::TooManyValues => "TOO_MANY_VALUES", ErrorKind::TooFewValues => "TOO_FEW_VALUES", ErrorKind::ArgumentConflict => "ARGUMENT_CONFLICT", ErrorKind::MissingSubcommand => "MISSING_COMMAND", ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => "HELP_REQUESTED", _ => "PARSE_ERROR", } } /// Extract the invalid subcommand from a clap error (Phase 4) fn extract_invalid_subcommand(e: &clap::Error) -> Option { // Parse the error message to find the invalid subcommand // Format is typically: "error: unrecognized subcommand 'foo'" let msg = e.to_string(); if let Some(start) = msg.find('\'') && let Some(end) = msg[start + 1..].find('\'') { return Some(msg[start + 1..start + 1 + end].to_string()); } None } /// Extract the invalid flag from a clap UnknownArgument error. /// Format is typically: "error: unexpected argument '--xyzzy' found" fn extract_invalid_flag(e: &clap::Error) -> Option { let msg = e.to_string(); if let Some(start) = msg.find('\'') && let Some(end) = msg[start + 1..].find('\'') { let value = &msg[start + 1..start + 1 + end]; if value.starts_with('-') { return Some(value.to_string()); } } None } /// Extract flag name and valid values from a clap InvalidValue error. /// Returns (flag_name, valid_values_if_listed_in_error). fn extract_invalid_value_context(e: &clap::Error) -> (Option, Option>) { let msg = e.to_string(); // Try to find the flag name from "[possible values: ...]" pattern or from the arg info // Clap format: "error: invalid value 'opend' for '--state '" let flag = if let Some(for_pos) = msg.find("for '") { let after_for = &msg[for_pos + 5..]; if let Some(end) = after_for.find('\'') { let raw = &after_for[..end]; // Strip angle-bracket value placeholder: "--state " -> "--state" Some(raw.split_whitespace().next().unwrap_or(raw).to_string()) } else { None } } else { None }; // Try to extract possible values from the error message // Clap format: "[possible values: opened, closed, merged, locked, all]" let valid_values = if let Some(pv_pos) = msg.find("[possible values: ") { let after_pv = &msg[pv_pos + 18..]; after_pv.find(']').map(|end| { after_pv[..end] .split(", ") .map(|s| s.trim().to_string()) .collect() }) } else { // Fall back to our static registry flag.as_deref() .and_then(autocorrect::valid_values_for_flag) .map(|v| v.iter().map(|s| (*s).to_string()).collect()) }; (flag, valid_values) } /// Extract the subcommand context from a clap error for better suggestions. /// Looks at the error message to find which command was being invoked. fn extract_subcommand_from_context(e: &clap::Error) -> Option { let msg = e.to_string(); let known = [ "issues", "mrs", "notes", "search", "sync", "ingest", "count", "status", "auth", "doctor", "stats", "timeline", "who", "me", "drift", "related", "trace", "file-history", "generate-docs", "embed", "token", "cron", "init", "migrate", ]; for cmd in known { if msg.contains(&format!("lore {cmd}")) || msg.contains(&format!("'{cmd}'")) { return Some(cmd.to_string()); } } None } /// Phase 4: Suggest similar command using fuzzy matching fn suggest_similar_command(invalid: &str) -> String { // Primary commands + common aliases for fuzzy matching const VALID_COMMANDS: &[(&str, &str)] = &[ ("issues", "issues"), ("issue", "issues"), ("mrs", "mrs"), ("mr", "mrs"), ("merge-requests", "mrs"), ("search", "search"), ("find", "search"), ("query", "search"), ("sync", "sync"), ("ingest", "ingest"), ("count", "count"), ("status", "status"), ("auth", "auth"), ("doctor", "doctor"), ("version", "version"), ("init", "init"), ("stats", "stats"), ("stat", "stats"), ("generate-docs", "generate-docs"), ("embed", "embed"), ("migrate", "migrate"), ("health", "health"), ("robot-docs", "robot-docs"), ("completions", "completions"), ("timeline", "timeline"), ("who", "who"), ("notes", "notes"), ("note", "notes"), ("drift", "drift"), ("file-history", "file-history"), ("trace", "trace"), ("related", "related"), ("me", "me"), ("token", "token"), ("cron", "cron"), // Hidden but may be known to agents ("list", "list"), ("show", "show"), ("reset", "reset"), ("backup", "backup"), ]; let invalid_lower = invalid.to_lowercase(); // Find the best match using Jaro-Winkler similarity let best_match = VALID_COMMANDS .iter() .map(|(alias, canonical)| (*canonical, jaro_winkler(&invalid_lower, alias))) .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); if let Some((cmd, score)) = best_match && score > 0.7 { let example = command_example(cmd); return format!( "Did you mean 'lore {cmd}'? Example: {example}. Run 'lore robot-docs' for all commands" ); } "Run 'lore robot-docs' for valid commands. Common: issues, mrs, search, sync, timeline, who" .to_string() } /// Return a contextual usage example for a command. fn command_example(cmd: &str) -> &'static str { match cmd { "issues" => "lore --robot issues -n 10", "mrs" => "lore --robot mrs -n 10", "search" => "lore --robot search \"auth bug\"", "sync" => "lore --robot sync", "ingest" => "lore --robot ingest issues", "notes" => "lore --robot notes --for-issue 123", "count" => "lore --robot count issues", "status" => "lore --robot status", "stats" => "lore --robot stats", "timeline" => "lore --robot timeline \"auth flow\"", "who" => "lore --robot who --path src/", "health" => "lore --robot health", "generate-docs" => "lore --robot generate-docs", "embed" => "lore --robot embed", "robot-docs" => "lore robot-docs", "trace" => "lore --robot trace src/main.rs", "init" => "lore init", "related" => "lore --robot related issues 42 -n 5", "me" => "lore --robot me", "drift" => "lore --robot drift issues 42", "file-history" => "lore --robot file-history src/main.rs", "token" => "lore --robot token show", "cron" => "lore --robot cron status", "auth" => "lore --robot auth", "doctor" => "lore --robot doctor", "migrate" => "lore --robot migrate", "completions" => "lore completions bash", _ => "lore --robot ", } }