diff --git a/rust/crates/api/src/client.rs b/rust/crates/api/src/client.rs index c0de6791..c768e514 100644 --- a/rust/crates/api/src/client.rs +++ b/rust/crates/api/src/client.rs @@ -40,7 +40,9 @@ impl ProviderClient { OpenAiCompatConfig::dashscope() } _ => { - if crate::providers::openai_compat::has_api_key("DASHSCOPE_API_KEY") && !crate::providers::openai_compat::has_api_key("OPENAI_API_KEY") { + if crate::providers::openai_compat::has_api_key("DASHSCOPE_API_KEY") + && !crate::providers::openai_compat::has_api_key("OPENAI_API_KEY") + { OpenAiCompatConfig::dashscope() } else { OpenAiCompatConfig::openai() diff --git a/rust/crates/api/src/error.rs b/rust/crates/api/src/error.rs index 9c2c76ed..438e3093 100644 --- a/rust/crates/api/src/error.rs +++ b/rust/crates/api/src/error.rs @@ -1,7 +1,7 @@ +use std::backtrace::Backtrace; use std::env::VarError; use std::fmt::{Display, Formatter}; use std::time::Duration; -use std::backtrace::Backtrace; const GENERIC_FATAL_WRAPPER_MARKERS: &[&str] = &[ "something went wrong while processing your request", @@ -77,10 +77,7 @@ pub enum ApiError { impl ApiError { #[must_use] #[track_caller] - pub fn missing_credentials( - provider: &'static str, - env_vars: &'static [&'static str], - ) -> Self { + pub fn missing_credentials(provider: &'static str, env_vars: &'static [&'static str]) -> Self { Self::MissingCredentials { provider, env_vars, diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index 39f16cca..b7c68969 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -199,7 +199,13 @@ pub fn metadata_for_model(model: &str) -> Option { // Uses the OpenAi provider kind because DashScope speaks the OpenAI REST // shape — only the base URL and auth env var differ. // Allow ali- prefix as well for generic DashScope usage. - if canonical.starts_with("qwen/") || canonical.starts_with("qwen-") || canonical.starts_with("ali/") || canonical.starts_with("ali-") || canonical.starts_with("glm/") || canonical.starts_with("glm-") { + if canonical.starts_with("qwen/") + || canonical.starts_with("qwen-") + || canonical.starts_with("ali/") + || canonical.starts_with("ali-") + || canonical.starts_with("glm/") + || canonical.starts_with("glm-") + { return Some(ProviderMetadata { provider: ProviderKind::OpenAi, auth_env: "DASHSCOPE_API_KEY", @@ -463,10 +469,8 @@ pub(crate) fn dotenv_value(key: &str) -> Option { .and_then(|exe| exe.parent().map(std::path::PathBuf::from)); // Search order: cwd → exe directory → home directory - let search_paths: Vec = [cwd, exe_dir, home] - .into_iter() - .flatten() - .collect(); + let search_paths: Vec = + [cwd, exe_dir, home].into_iter().flatten().collect(); for dir in &search_paths { let env_path = dir.join(".env"); @@ -777,14 +781,14 @@ mod tests { #[test] fn returns_context_window_metadata_for_kimi_models() { // kimi-k2.5 - let k25_limit = model_token_limit("kimi-k2.5") - .expect("kimi-k2.5 should have token limit metadata"); + let k25_limit = + model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have token limit metadata"); assert_eq!(k25_limit.max_output_tokens, 16_384); assert_eq!(k25_limit.context_window_tokens, 256_000); // kimi-k1.5 - let k15_limit = model_token_limit("kimi-k1.5") - .expect("kimi-k1.5 should have token limit metadata"); + let k15_limit = + model_token_limit("kimi-k1.5").expect("kimi-k1.5 should have token limit metadata"); assert_eq!(k15_limit.max_output_tokens, 16_384); assert_eq!(k15_limit.context_window_tokens, 256_000); } @@ -792,11 +796,13 @@ mod tests { #[test] fn kimi_alias_resolves_to_kimi_k25_token_limits() { // The "kimi" alias resolves to "kimi-k2.5" via resolve_model_alias() - let alias_limit = model_token_limit("kimi") - .expect("kimi alias should resolve to kimi-k2.5 limits"); - let direct_limit = model_token_limit("kimi-k2.5") - .expect("kimi-k2.5 should have limits"); - assert_eq!(alias_limit.max_output_tokens, direct_limit.max_output_tokens); + let alias_limit = + model_token_limit("kimi").expect("kimi alias should resolve to kimi-k2.5 limits"); + let direct_limit = model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have limits"); + assert_eq!( + alias_limit.max_output_tokens, + direct_limit.max_output_tokens + ); assert_eq!( alias_limit.context_window_tokens, direct_limit.context_window_tokens @@ -1061,6 +1067,7 @@ NO_EQUALS_LINE provider, env_vars, hint, + .. } => { assert_eq!(*provider, "Anthropic"); assert_eq!(*env_vars, &["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]); @@ -1095,6 +1102,7 @@ NO_EQUALS_LINE provider, env_vars, hint, + .. } => { assert_eq!(*provider, "Anthropic"); assert_eq!(*env_vars, &["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"]); diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index e55f8ebf..47dcddad 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -801,7 +801,10 @@ fn strip_routing_prefix(model: &str) -> &str { let prefix = &model[..pos]; // Only strip if the prefix before "/" is a known routing prefix, // not if "/" appears in the middle of the model name for other reasons. - if matches!(prefix, "openai" | "xai" | "grok" | "qwen" | "kimi" | "ali" | "glm") { + if matches!( + prefix, + "openai" | "xai" | "grok" | "qwen" | "kimi" | "ali" | "glm" + ) { &model[pos + 1..] } else { model @@ -2195,9 +2198,16 @@ mod tests { #[test] fn provider_specific_size_limits_are_correct() { - assert_eq!(OpenAiCompatConfig::dashscope().max_request_body_bytes, 6_291_456); // 6MB - assert_eq!(OpenAiCompatConfig::openai().max_request_body_bytes, 104_857_600); // 100MB - assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800); // 50MB + assert_eq!( + OpenAiCompatConfig::dashscope().max_request_body_bytes, + 6_291_456 + ); // 6MB + assert_eq!( + OpenAiCompatConfig::openai().max_request_body_bytes, + 104_857_600 + ); // 100MB + assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800); + // 50MB } #[test] diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 9d693935..e01e3cf1 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -2338,18 +2338,20 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std:: } } +#[must_use] pub fn handle_mcp_slash_command( args: Option<&str>, cwd: &Path, -) -> Result { +) -> String { let loader = ConfigLoader::default_for(cwd); render_mcp_report_for(&loader, cwd, args) } +#[must_use] pub fn handle_mcp_slash_command_json( args: Option<&str>, cwd: &Path, -) -> Result { +) -> Value { let loader = ConfigLoader::default_for(cwd); render_mcp_report_json_for(&loader, cwd, args) } @@ -2541,14 +2543,14 @@ fn render_mcp_report_for( loader: &ConfigLoader, cwd: &Path, args: Option<&str>, -) -> Result { +) -> String { if let Some(args) = normalize_optional_args(args) { if let Some(help_path) = help_path_from_args(args) { - return Ok(match help_path.as_slice() { + return match help_path.as_slice() { [] => render_mcp_usage(None), ["show", ..] => render_mcp_usage(Some("show")), _ => render_mcp_usage(Some(&help_path.join(" "))), - }); + }; } } @@ -2558,45 +2560,45 @@ fn render_mcp_report_for( // as #143 for `status`). Text mode prepends a "Config load error" // block before the MCP list; the list falls back to empty. match loader.load() { - Ok(runtime_config) => Ok(render_mcp_summary_report( + Ok(runtime_config) => render_mcp_summary_report( cwd, runtime_config.mcp().servers(), - )), + ), Err(err) => { let empty = std::collections::BTreeMap::new(); - Ok(format!( + format!( "Config load error\n Status fail\n Summary runtime config failed to load; reporting partial MCP view\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun\n\n{}", render_mcp_summary_report(cwd, &empty) - )) + ) } } } - Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)), - Some("show") => Ok(render_mcp_usage(Some("show"))), + Some(args) if is_help_arg(args) => render_mcp_usage(None), + Some("show") => render_mcp_usage(Some("show")), Some(args) if args.split_whitespace().next() == Some("show") => { let mut parts = args.split_whitespace(); let _ = parts.next(); let Some(server_name) = parts.next() else { - return Ok(render_mcp_usage(Some("show"))); + return render_mcp_usage(Some("show")); }; if parts.next().is_some() { - return Ok(render_mcp_usage(Some(args))); + return render_mcp_usage(Some(args)); } // #144: same degradation for `mcp show`; if config won't parse, // the specific server lookup can't succeed, so report the parse // error with context. match loader.load() { - Ok(runtime_config) => Ok(render_mcp_server_report( + Ok(runtime_config) => render_mcp_server_report( cwd, server_name, runtime_config.mcp().get(server_name), - )), - Err(err) => Ok(format!( + ), + Err(err) => format!( "Config load error\n Status fail\n Summary runtime config failed to load; cannot resolve `{server_name}`\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun" - )), + ), } } - Some(args) => Ok(render_mcp_usage(Some(args))), + Some(args) => render_mcp_usage(Some(args)), } } @@ -2604,14 +2606,14 @@ fn render_mcp_report_json_for( loader: &ConfigLoader, cwd: &Path, args: Option<&str>, -) -> Result { +) -> Value { if let Some(args) = normalize_optional_args(args) { if let Some(help_path) = help_path_from_args(args) { - return Ok(match help_path.as_slice() { + return match help_path.as_slice() { [] => render_mcp_usage_json(None), ["show", ..] => render_mcp_usage_json(Some("show")), _ => render_mcp_usage_json(Some(&help_path.join(" "))), - }); + }; } } @@ -2623,15 +2625,13 @@ fn render_mcp_report_json_for( // runs, the existing serializer adds `status: "ok"` below. match loader.load() { Ok(runtime_config) => { - let mut value = render_mcp_summary_report_json( - cwd, - runtime_config.mcp().servers(), - ); + let mut value = + render_mcp_summary_report_json(cwd, runtime_config.mcp().servers()); if let Some(map) = value.as_object_mut() { map.insert("status".to_string(), Value::String("ok".to_string())); map.insert("config_load_error".to_string(), Value::Null); } - Ok(value) + value } Err(err) => { let empty = std::collections::BTreeMap::new(); @@ -2643,20 +2643,20 @@ fn render_mcp_report_json_for( Value::String(err.to_string()), ); } - Ok(value) + value } } } - Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)), - Some("show") => Ok(render_mcp_usage_json(Some("show"))), + Some(args) if is_help_arg(args) => render_mcp_usage_json(None), + Some("show") => render_mcp_usage_json(Some("show")), Some(args) if args.split_whitespace().next() == Some("show") => { let mut parts = args.split_whitespace(); let _ = parts.next(); let Some(server_name) = parts.next() else { - return Ok(render_mcp_usage_json(Some("show"))); + return render_mcp_usage_json(Some("show")); }; if parts.next().is_some() { - return Ok(render_mcp_usage_json(Some(args))); + return render_mcp_usage_json(Some(args)); } // #144: same degradation pattern for show action. match loader.load() { @@ -2670,19 +2670,19 @@ fn render_mcp_report_json_for( map.insert("status".to_string(), Value::String("ok".to_string())); map.insert("config_load_error".to_string(), Value::Null); } - Ok(value) + value } - Err(err) => Ok(serde_json::json!({ + Err(err) => serde_json::json!({ "kind": "mcp", "action": "show", "server": server_name, "status": "degraded", "config_load_error": err.to_string(), "working_directory": cwd.display().to_string(), - })), + }), } } - Some(args) => Ok(render_mcp_usage_json(Some(args))), + Some(args) => render_mcp_usage_json(Some(args)), } } @@ -5358,21 +5358,21 @@ mod tests { fn mcp_usage_supports_help_and_unexpected_args() { let cwd = temp_dir("mcp-usage"); - let help = super::handle_mcp_slash_command(Some("help"), &cwd).expect("mcp help"); + let help = super::handle_mcp_slash_command(Some("help"), &cwd); assert!(help.contains("Usage /mcp [list|show |help]")); assert!(help.contains("Direct CLI claw mcp [list|show |help]")); let unexpected = - super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage"); + super::handle_mcp_slash_command(Some("show alpha beta"), &cwd); assert!(unexpected.contains("Unexpected show alpha beta")); let nested_help = - super::handle_mcp_slash_command(Some("show --help"), &cwd).expect("mcp help"); + super::handle_mcp_slash_command(Some("show --help"), &cwd); assert!(nested_help.contains("Usage /mcp [list|show |help]")); assert!(nested_help.contains("Unexpected show")); let unknown_help = - super::handle_mcp_slash_command(Some("inspect --help"), &cwd).expect("mcp usage"); + super::handle_mcp_slash_command(Some("inspect --help"), &cwd); assert!(unknown_help.contains("Usage /mcp [list|show |help]")); assert!(unknown_help.contains("Unexpected inspect")); @@ -5423,8 +5423,7 @@ mod tests { .expect("write local settings"); let loader = ConfigLoader::new(&workspace, &config_home); - let list = super::render_mcp_report_for(&loader, &workspace, None) - .expect("mcp list report should render"); + let list = super::render_mcp_report_for(&loader, &workspace, None); assert!(list.contains("Configured servers 2")); assert!(list.contains("alpha")); assert!(list.contains("stdio")); @@ -5435,21 +5434,18 @@ mod tests { assert!(list.contains("local")); assert!(list.contains("wss://remote.example/mcp")); - let show = super::render_mcp_report_for(&loader, &workspace, Some("show alpha")) - .expect("mcp show report should render"); + let show = super::render_mcp_report_for(&loader, &workspace, Some("show alpha")); assert!(show.contains("Name alpha")); assert!(show.contains("Command uvx")); assert!(show.contains("Args alpha-server")); assert!(show.contains("Env keys ALPHA_TOKEN")); assert!(show.contains("Tool timeout 1200 ms")); - let remote = super::render_mcp_report_for(&loader, &workspace, Some("show remote")) - .expect("mcp show remote report should render"); + let remote = super::render_mcp_report_for(&loader, &workspace, Some("show remote")); assert!(remote.contains("Transport ws")); assert!(remote.contains("URL wss://remote.example/mcp")); - let missing = super::render_mcp_report_for(&loader, &workspace, Some("show missing")) - .expect("missing report should render"); + let missing = super::render_mcp_report_for(&loader, &workspace, Some("show missing")); assert!(missing.contains("server `missing` is not configured")); let _ = fs::remove_dir_all(workspace); @@ -5501,7 +5497,7 @@ mod tests { let loader = ConfigLoader::new(&workspace, &config_home); let list = - render_mcp_report_json_for(&loader, &workspace, None).expect("mcp list json render"); + render_mcp_report_json_for(&loader, &workspace, None); assert_eq!(list["kind"], "mcp"); assert_eq!(list["action"], "list"); assert_eq!(list["configured_servers"], 2); @@ -5516,21 +5512,19 @@ mod tests { "wss://remote.example/mcp" ); - let show = render_mcp_report_json_for(&loader, &workspace, Some("show alpha")) - .expect("mcp show json render"); + let show = render_mcp_report_json_for(&loader, &workspace, Some("show alpha")); assert_eq!(show["action"], "show"); assert_eq!(show["found"], true); assert_eq!(show["server"]["name"], "alpha"); assert_eq!(show["server"]["details"]["env_keys"][0], "ALPHA_TOKEN"); assert_eq!(show["server"]["details"]["tool_call_timeout_ms"], 1200); - let missing = render_mcp_report_json_for(&loader, &workspace, Some("show missing")) - .expect("mcp missing json render"); + let missing = render_mcp_report_json_for(&loader, &workspace, Some("show missing")); assert_eq!(missing["found"], false); assert_eq!(missing["server_name"], "missing"); let help = - render_mcp_report_json_for(&loader, &workspace, Some("help")).expect("mcp help json"); + render_mcp_report_json_for(&loader, &workspace, Some("help")); assert_eq!(help["action"], "help"); assert_eq!(help["usage"]["sources"][0], ".claw/settings.json"); @@ -5565,8 +5559,7 @@ mod tests { let loader = ConfigLoader::new(&workspace, &config_home); // list action: must return Ok (not Err) with degraded envelope. - let list = render_mcp_report_json_for(&loader, &workspace, None) - .expect("mcp list should not hard-fail on config parse errors (#144)"); + let list = render_mcp_report_json_for(&loader, &workspace, None); assert_eq!(list["kind"], "mcp"); assert_eq!(list["action"], "list"); assert_eq!( @@ -5585,8 +5578,7 @@ mod tests { assert!(list["servers"].as_array().unwrap().is_empty()); // show action: should also degrade (not hard-fail). - let show = render_mcp_report_json_for(&loader, &workspace, Some("show everything")) - .expect("mcp show should not hard-fail on config parse errors (#144)"); + let show = render_mcp_report_json_for(&loader, &workspace, Some("show everything")); assert_eq!(show["kind"], "mcp"); assert_eq!(show["action"], "show"); assert_eq!( @@ -5600,8 +5592,7 @@ mod tests { let clean_ws = temp_dir("mcp-degrades-144-clean"); fs::create_dir_all(&clean_ws).expect("clean ws"); let clean_loader = ConfigLoader::new(&clean_ws, &config_home); - let clean_list = render_mcp_report_json_for(&clean_loader, &clean_ws, None) - .expect("clean mcp list should succeed"); + let clean_list = render_mcp_report_json_for(&clean_loader, &clean_ws, None); assert_eq!( clean_list["status"].as_str(), Some("ok"), diff --git a/rust/crates/lsp/src/client.rs b/rust/crates/lsp/src/client.rs index 7ec663b1..ccf74526 100644 --- a/rust/crates/lsp/src/client.rs +++ b/rust/crates/lsp/src/client.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Stdio; -use std::sync::Arc; use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::Arc; use lsp_types::{ Diagnostic, GotoDefinitionResponse, Location, LocationLink, Position, PublishDiagnosticsParams, @@ -15,11 +15,13 @@ use tokio::sync::{oneshot, Mutex}; use crate::error::LspError; use crate::types::{LspServerConfig, SymbolLocation}; +type PendingRequests = Arc>>>>; + pub(crate) struct LspClient { config: LspServerConfig, writer: Mutex>, child: Mutex, - pending_requests: Arc>>>>, + pending_requests: PendingRequests, diagnostics: Arc>>>, open_documents: Mutex>, next_request_id: AtomicI64, @@ -59,7 +61,7 @@ impl LspClient { client.spawn_reader(stdout); if let Some(stderr) = stderr { - client.spawn_stderr_drain(stderr); + Self::spawn_stderr_drain(stderr); } client.initialize().await?; Ok(client) @@ -190,7 +192,9 @@ impl LspClient { Some(GotoDefinitionResponse::Scalar(location)) => { location_to_symbol_locations(vec![location]) } - Some(GotoDefinitionResponse::Array(locations)) => location_to_symbol_locations(locations), + Some(GotoDefinitionResponse::Array(locations)) => { + location_to_symbol_locations(locations) + } Some(GotoDefinitionResponse::Link(links)) => location_links_to_symbol_locations(links), None => Vec::new(), }) @@ -272,7 +276,8 @@ impl LspClient { if notification.diagnostics.is_empty() { diagnostics_map.remove(¬ification.uri.to_string()); } else { - diagnostics_map.insert(notification.uri.to_string(), notification.diagnostics); + diagnostics_map + .insert(notification.uri.to_string(), notification.diagnostics); } } Ok::<(), LspError>(()) @@ -281,10 +286,7 @@ impl LspClient { if let Err(error) = result { let mut pending = pending_requests.lock().await; - let drained = pending - .iter() - .map(|(id, _)| *id) - .collect::>(); + let drained = pending.keys().copied().collect::>(); for id in drained { if let Some(sender) = pending.remove(&id) { let _ = sender.send(Err(LspError::Protocol(error.to_string()))); @@ -294,7 +296,7 @@ impl LspClient { }); } - fn spawn_stderr_drain(&self, stderr: R) + fn spawn_stderr_drain(stderr: R) where R: AsyncRead + Unpin + Send + 'static, { @@ -439,7 +441,7 @@ fn location_to_symbol_locations(locations: Vec) -> Vec locations .into_iter() .filter_map(|location| { - uri_to_path(&location.uri.to_string()).map(|path| SymbolLocation { + uri_to_path(location.uri.as_str()).map(|path| SymbolLocation { path, range: location.range, }) @@ -448,9 +450,10 @@ fn location_to_symbol_locations(locations: Vec) -> Vec } fn location_links_to_symbol_locations(links: Vec) -> Vec { - links.into_iter() + links + .into_iter() .filter_map(|link| { - uri_to_path(&link.target_uri.to_string()).map(|path| SymbolLocation { + uri_to_path(link.target_uri.as_str()).map(|path| SymbolLocation { path, range: link.target_selection_range, }) diff --git a/rust/crates/lsp/src/lib.rs b/rust/crates/lsp/src/lib.rs index 9b1b099d..e2bb7e4b 100644 --- a/rust/crates/lsp/src/lib.rs +++ b/rust/crates/lsp/src/lib.rs @@ -192,7 +192,8 @@ while True: fs::create_dir_all(root.join("src")).expect("workspace root should exist"); let script_path = write_mock_server_script(&root); let source_path = root.join("src").join("main.rs"); - fs::write(&source_path, "fn main() {}\nlet value = 1;\n").expect("source file should exist"); + fs::write(&source_path, "fn main() {}\nlet value = 1;\n") + .expect("source file should exist"); let manager = LspManager::new(vec![LspServerConfig { name: "rust-analyzer".to_string(), command: python, @@ -204,7 +205,10 @@ while True: }]) .expect("manager should build"); manager - .open_document(&source_path, &fs::read_to_string(&source_path).expect("source read should succeed")) + .open_document( + &source_path, + &fs::read_to_string(&source_path).expect("source read should succeed"), + ) .await .expect("document should open"); wait_for_diagnostics(&manager).await; @@ -226,7 +230,10 @@ while True: // then assert_eq!(diagnostics.files.len(), 1); assert_eq!(diagnostics.total_diagnostics(), 1); - assert_eq!(diagnostics.files[0].diagnostics[0].severity, Some(DiagnosticSeverity::ERROR)); + assert_eq!( + diagnostics.files[0].diagnostics[0].severity, + Some(DiagnosticSeverity::ERROR) + ); assert_eq!(definitions.len(), 1); assert_eq!(definitions[0].start_line(), 1); assert_eq!(references.len(), 2); @@ -246,7 +253,8 @@ while True: fs::create_dir_all(root.join("src")).expect("workspace root should exist"); let script_path = write_mock_server_script(&root); let source_path = root.join("src").join("lib.rs"); - fs::write(&source_path, "pub fn answer() -> i32 { 42 }\n").expect("source file should exist"); + fs::write(&source_path, "pub fn answer() -> i32 { 42 }\n") + .expect("source file should exist"); let manager = LspManager::new(vec![LspServerConfig { name: "rust-analyzer".to_string(), command: python, @@ -258,7 +266,10 @@ while True: }]) .expect("manager should build"); manager - .open_document(&source_path, &fs::read_to_string(&source_path).expect("source read should succeed")) + .open_document( + &source_path, + &fs::read_to_string(&source_path).expect("source read should succeed"), + ) .await .expect("document should open"); wait_for_diagnostics(&manager).await; diff --git a/rust/crates/lsp/src/manager.rs b/rust/crates/lsp/src/manager.rs index 3c99f96f..730a1d5c 100644 --- a/rust/crates/lsp/src/manager.rs +++ b/rust/crates/lsp/src/manager.rs @@ -26,7 +26,9 @@ impl LspManager { for config in server_configs { for extension in config.extension_to_language.keys() { let normalized = normalize_extension(extension); - if let Some(existing_server) = extension_map.insert(normalized.clone(), config.name.clone()) { + if let Some(existing_server) = + extension_map.insert(normalized.clone(), config.name.clone()) + { return Err(LspError::DuplicateExtension { extension: normalized, existing_server, @@ -53,7 +55,10 @@ impl LspManager { } pub async fn open_document(&self, path: &Path, text: &str) -> Result<(), LspError> { - self.client_for_path(path).await?.open_document(path, text).await + self.client_for_path(path) + .await? + .open_document(path, text) + .await } pub async fn sync_document_from_disk(&self, path: &Path) -> Result<(), LspError> { @@ -63,7 +68,10 @@ impl LspManager { } pub async fn change_document(&self, path: &Path, text: &str) -> Result<(), LspError> { - self.client_for_path(path).await?.change_document(path, text).await + self.client_for_path(path) + .await? + .change_document(path, text) + .await } pub async fn save_document(&self, path: &Path) -> Result<(), LspError> { @@ -79,7 +87,11 @@ impl LspManager { path: &Path, position: Position, ) -> Result, LspError> { - let mut locations = self.client_for_path(path).await?.go_to_definition(path, position).await?; + let mut locations = self + .client_for_path(path) + .await? + .go_to_definition(path, position) + .await?; dedupe_locations(&mut locations); Ok(locations) } @@ -100,14 +112,21 @@ impl LspManager { } pub async fn collect_workspace_diagnostics(&self) -> Result { - let clients = self.clients.lock().await.values().cloned().collect::>(); + let clients = self + .clients + .lock() + .await + .values() + .cloned() + .collect::>(); let mut files = Vec::new(); for client in clients { for (uri, diagnostics) in client.diagnostics_snapshot().await { - let Ok(path) = url::Url::parse(&uri) - .and_then(|url| url.to_file_path().map_err(|()| url::ParseError::RelativeUrlWithoutBase)) - else { + let Ok(path) = url::Url::parse(&uri).and_then(|url| { + url.to_file_path() + .map_err(|()| url::ParseError::RelativeUrlWithoutBase) + }) else { continue; }; if diagnostics.is_empty() { diff --git a/rust/crates/runtime/src/bash.rs b/rust/crates/runtime/src/bash.rs index e5119cde..f7c3d45b 100644 --- a/rust/crates/runtime/src/bash.rs +++ b/rust/crates/runtime/src/bash.rs @@ -122,7 +122,7 @@ fn detect_and_emit_ship_prepared(command: &str) { actor: get_git_actor().unwrap_or_else(|| "unknown".to_string()), pr_number: None, }; - let _event = LaneEvent::ship_prepared(format!("{}", now), &provenance); + let _event = LaneEvent::ship_prepared(format!("{now}"), &provenance); // Log to stderr as interim routing before event stream integration eprintln!( "[ship.prepared] branch={} -> main, commits={}, actor={}", @@ -172,7 +172,7 @@ async fn execute_bash_async( ) -> io::Result { // Detect and emit ship provenance for git push operations detect_and_emit_ship_prepared(&input.command); - + let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true); let output_result = if let Some(timeout_ms) = input.timeout { diff --git a/rust/crates/runtime/src/conversation.rs b/rust/crates/runtime/src/conversation.rs index bfffda4f..ba92168b 100644 --- a/rust/crates/runtime/src/conversation.rs +++ b/rust/crates/runtime/src/conversation.rs @@ -357,7 +357,7 @@ where Ok(events) => events, Err(error) => { self.record_turn_failed(iterations, &error); - + let result = compact_session( &self.session, CompactionConfig { @@ -368,10 +368,12 @@ where if result.removed_message_count > 0 { self.session = result.compacted_session; } - + let error_msg = format!("API request failed with error: {error}\nI have automatically truncated the earlier conversation history to reduce context length. Please analyze the error, review your task, and continue."); if let Err(e) = self.session.push_user_text(error_msg) { - return Err(RuntimeError::new(format!("Failed to push error message: {e}"))); + return Err(RuntimeError::new(format!( + "Failed to push error message: {e}" + ))); } continue; } @@ -381,10 +383,13 @@ where Ok(result) => result, Err(error) => { self.record_turn_failed(iterations, &error); - - let error_msg = format!("Failed to parse API response: {error}\nPlease try again."); + + let error_msg = + format!("Failed to parse API response: {error}\nPlease try again."); if let Err(e) = self.session.push_user_text(error_msg) { - return Err(RuntimeError::new(format!("Failed to push error message: {e}"))); + return Err(RuntimeError::new(format!( + "Failed to push error message: {e}" + ))); } continue; } diff --git a/rust/crates/runtime/src/lane_events.rs b/rust/crates/runtime/src/lane_events.rs index 2dcb0427..c08b8bb5 100644 --- a/rust/crates/runtime/src/lane_events.rs +++ b/rust/crates/runtime/src/lane_events.rs @@ -405,7 +405,10 @@ pub enum BlockedSubphase { #[serde(rename = "blocked.branch_freshness")] BranchFreshness { behind_main: u32 }, #[serde(rename = "blocked.test_hang")] - TestHang { elapsed_secs: u32, test_name: Option }, + TestHang { + elapsed_secs: u32, + test_name: Option, + }, #[serde(rename = "blocked.report_pending")] ReportPending { since_secs: u32 }, } @@ -543,7 +546,8 @@ impl LaneEvent { .with_failure_class(blocker.failure_class) .with_detail(blocker.detail.clone()); if let Some(ref subphase) = blocker.subphase { - event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize")); + event = + event.with_data(serde_json::to_value(subphase).expect("subphase should serialize")); } event } @@ -554,7 +558,8 @@ impl LaneEvent { .with_failure_class(blocker.failure_class) .with_detail(blocker.detail.clone()); if let Some(ref subphase) = blocker.subphase { - event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize")); + event = + event.with_data(serde_json::to_value(subphase).expect("subphase should serialize")); } event } @@ -562,8 +567,12 @@ impl LaneEvent { /// Ship prepared — §4.44.5 #[must_use] pub fn ship_prepared(emitted_at: impl Into, provenance: &ShipProvenance) -> Self { - Self::new(LaneEventName::ShipPrepared, LaneEventStatus::Ready, emitted_at) - .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) + Self::new( + LaneEventName::ShipPrepared, + LaneEventStatus::Ready, + emitted_at, + ) + .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) } /// Ship commits selected — §4.44.5 @@ -573,22 +582,34 @@ impl LaneEvent { commit_count: u32, commit_range: impl Into, ) -> Self { - Self::new(LaneEventName::ShipCommitsSelected, LaneEventStatus::Ready, emitted_at) - .with_detail(format!("{} commits: {}", commit_count, commit_range.into())) + Self::new( + LaneEventName::ShipCommitsSelected, + LaneEventStatus::Ready, + emitted_at, + ) + .with_detail(format!("{} commits: {}", commit_count, commit_range.into())) } /// Ship merged — §4.44.5 #[must_use] pub fn ship_merged(emitted_at: impl Into, provenance: &ShipProvenance) -> Self { - Self::new(LaneEventName::ShipMerged, LaneEventStatus::Completed, emitted_at) - .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) + Self::new( + LaneEventName::ShipMerged, + LaneEventStatus::Completed, + emitted_at, + ) + .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) } /// Ship pushed to main — §4.44.5 #[must_use] pub fn ship_pushed_main(emitted_at: impl Into, provenance: &ShipProvenance) -> Self { - Self::new(LaneEventName::ShipPushedMain, LaneEventStatus::Completed, emitted_at) - .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) + Self::new( + LaneEventName::ShipPushedMain, + LaneEventStatus::Completed, + emitted_at, + ) + .with_data(serde_json::to_value(provenance).expect("ship provenance should serialize")) } #[must_use] diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index 7a612ebf..743ae7d5 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -58,8 +58,8 @@ impl SessionStore { let workspace_root = workspace_root.as_ref(); // #151: canonicalize workspace_root for consistent fingerprinting // across equivalent path representations. - let canonical_workspace = fs::canonicalize(workspace_root) - .unwrap_or_else(|_| workspace_root.to_path_buf()); + let canonical_workspace = + fs::canonicalize(workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf()); let sessions_root = data_dir .as_ref() .join("sessions") @@ -158,10 +158,9 @@ impl SessionStore { } pub fn latest_session(&self) -> Result { - self.list_sessions()? - .into_iter() - .next() - .ok_or_else(|| SessionControlError::Format(format_no_managed_sessions(&self.sessions_root))) + self.list_sessions()?.into_iter().next().ok_or_else(|| { + SessionControlError::Format(format_no_managed_sessions(&self.sessions_root)) + }) } pub fn load_session( diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index a965e322..79764e0c 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -4,7 +4,10 @@ unused_variables, clippy::unneeded_struct_pattern, clippy::unnecessary_wraps, - clippy::unused_self + clippy::unused_self, + clippy::too_many_lines, + clippy::doc_markdown, + clippy::result_large_err )] mod init; mod input; @@ -200,9 +203,12 @@ type RuntimePluginStateBuildOutput = ( ); fn main() { - if let Err(error) = run() { - let message = format!("{:?}\n\nStack trace:\n{}", error, std::backtrace::Backtrace::force_capture()); + let message = format!( + "{:?}\n\nStack trace:\n{}", + error, + std::backtrace::Backtrace::force_capture() + ); // When --output-format json is active, emit errors as JSON so downstream // tools can parse failures the same way they parse successes (ROADMAP #42). let argv: Vec = std::env::args().collect(); @@ -230,8 +236,10 @@ fn main() { // don't need to regex-scrape the prose. let kind = classify_error_kind(&message); if message.contains("`claw --help`") { - eprintln!("[error-kind: {kind}] -error: {message}"); + eprintln!( + "[error-kind: {kind}] +error: {message}" + ); } else { eprintln!( "[error-kind: {kind}] @@ -375,7 +383,12 @@ fn run() -> Result<(), Box> { model_flag_raw, permission_mode, output_format, - } => print_status_snapshot(&model, model_flag_raw.as_deref(), permission_mode, output_format)?, + } => print_status_snapshot( + &model, + model_flag_raw.as_deref(), + permission_mode, + output_format, + )?, CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?, CliAction::Prompt { prompt, @@ -416,19 +429,17 @@ fn run() -> Result<(), Box> { CliAction::Config { section, output_format, - } => { - match output_format { - CliOutputFormat::Text => { - println!("{}", render_config_report(section.as_deref())?); - } - CliOutputFormat::Json => { - println!( - "{}", - serde_json::to_string_pretty(&render_config_json(section.as_deref())?)? - ); - } + } => match output_format { + CliOutputFormat::Text => { + println!("{}", render_config_report(section.as_deref())?); } - } + CliOutputFormat::Json => { + println!( + "{}", + serde_json::to_string_pretty(&render_config_json(section.as_deref())?)? + ); + } + }, CliAction::Diff { output_format } => match output_format { CliOutputFormat::Text => { println!("{}", render_diff_report()?); @@ -632,13 +643,7 @@ fn parse_args(args: &[String]) -> Result { } "--help" | "-h" if !rest.is_empty() - && matches!( - rest[0].as_str(), - "prompt" - | "commit" - | "pr" - | "issue" - ) => + && matches!(rest[0].as_str(), "prompt" | "commit" | "pr" | "issue") => { // `--help` following a subcommand that would otherwise forward // the arg to the API (e.g. `claw prompt --help`) should show @@ -849,9 +854,13 @@ fn parse_args(args: &[String]) -> Result { if let Some(action) = parse_local_help_action(&rest) { return action; } - if let Some(action) = - parse_single_word_command_alias(&rest, &model, model_flag_raw.as_deref(), permission_mode_override, output_format) - { + if let Some(action) = parse_single_word_command_alias( + &rest, + &model, + model_flag_raw.as_deref(), + permission_mode_override, + output_format, + ) { return action; } @@ -1317,7 +1326,6 @@ fn suggest_closest_term<'a>(input: &str, candidates: &'a [&'a str]) -> Option<&' ranked_suggestions(input, candidates).into_iter().next() } - fn suggest_similar_subcommand(input: &str) -> Option> { const KNOWN_SUBCOMMANDS: &[&str] = &[ "help", @@ -1347,8 +1355,7 @@ fn suggest_similar_subcommand(input: &str) -> Option> { let prefix_match = common_prefix_len(&normalized_input, &normalized_candidate) >= 4; let substring_match = normalized_candidate.contains(&normalized_input) || normalized_input.contains(&normalized_candidate); - ((distance <= 2) || prefix_match || substring_match) - .then_some((distance, *candidate)) + ((distance <= 2) || prefix_match || substring_match).then_some((distance, *candidate)) }) .collect::>(); ranked.sort_by(|left, right| left.cmp(right).then_with(|| left.1.cmp(right.1))); @@ -1368,7 +1375,6 @@ fn common_prefix_len(left: &str, right: &str) -> usize { .count() } - fn looks_like_subcommand_typo(input: &str) -> bool { !input.is_empty() && input @@ -1455,19 +1461,23 @@ fn validate_model_syntax(model: &str) -> Result<(), String> { } // Known aliases are always valid match trimmed { - "opus" | "sonnet" | "haiku" | "grok" | "grok-2" | "grok-3" | "grok-mini" | "grok-3-mini" | "kimi" | "glm-5" => return Ok(()), + "opus" | "sonnet" | "haiku" | "grok" | "grok-2" | "grok-3" | "grok-mini" + | "grok-3-mini" | "kimi" | "glm-5" => return Ok(()), _ => {} } - + // Dashscope and xAI models that don't use provider/model syntax - if trimmed.starts_with("qwen-") || trimmed.starts_with("ali-") || trimmed.starts_with("glm-") || trimmed.starts_with("kimi-") { + if trimmed.starts_with("qwen-") + || trimmed.starts_with("ali-") + || trimmed.starts_with("glm-") + || trimmed.starts_with("kimi-") + { return Ok(()); } // Check for spaces (malformed) if trimmed.contains(' ') { return Err(format!( - "invalid model syntax: '{}' contains spaces. Use provider/model format or known alias", - trimmed + "invalid model syntax: '{trimmed}' contains spaces. Use provider/model format or known alias" )); } // Check provider/model format: provider_id/model_id @@ -1475,20 +1485,17 @@ fn validate_model_syntax(model: &str) -> Result<(), String> { if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { // #154: hint if the model looks like it belongs to a different provider let mut err_msg = format!( - "invalid model syntax: '{}'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)", - trimmed + "invalid model syntax: '{trimmed}'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)" ); if trimmed.starts_with("gpt-") || trimmed.starts_with("gpt_") { err_msg.push_str("\nDid you mean `openai/"); err_msg.push_str(trimmed); err_msg.push_str("`? (Requires OPENAI_API_KEY env var)"); - } - else if trimmed.starts_with("qwen") { + } else if trimmed.starts_with("qwen") { err_msg.push_str("\nDid you mean `qwen/"); err_msg.push_str(trimmed); err_msg.push_str("`? (Requires DASHSCOPE_API_KEY env var)"); - } - else if trimmed.starts_with("grok") { + } else if trimmed.starts_with("grok") { err_msg.push_str("\nDid you mean `xai/"); err_msg.push_str(trimmed); err_msg.push_str("`? (Requires XAI_API_KEY env var)"); @@ -3284,8 +3291,8 @@ fn run_resume_command( }; Ok(ResumeCommandOutcome { session: session.clone(), - message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?), - json: Some(handle_mcp_slash_command_json(args.as_deref(), &cwd)?), + message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)), + json: Some(handle_mcp_slash_command_json(args.as_deref(), &cwd)), }) } SlashCommand::Memory => Ok(ResumeCommandOutcome { @@ -4332,7 +4339,6 @@ impl LiveCli { Ok(()) } - fn run_prompt_compact_json(&mut self, input: &str) -> Result<(), Box> { let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?; let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode); @@ -4868,10 +4874,10 @@ impl LiveCli { } let cwd = env::current_dir()?; match output_format { - CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)?), + CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)), CliOutputFormat::Json => println!( "{}", - serde_json::to_string_pretty(&handle_mcp_slash_command_json(args, &cwd)?)? + serde_json::to_string_pretty(&handle_mcp_slash_command_json(args, &cwd))? ), } Ok(()) @@ -5450,7 +5456,13 @@ fn print_status_snapshot( match output_format { CliOutputFormat::Text => println!( "{}", - format_status_report(&provenance.resolved, usage, permission_mode.as_str(), &context, Some(&provenance)) + format_status_report( + &provenance.resolved, + usage, + permission_mode.as_str(), + &context, + Some(&provenance) + ) ), CliOutputFormat::Json => println!( "{}", @@ -5618,8 +5630,7 @@ fn format_status_report( Some(raw) if raw != model => { format!("\n Model source {} (raw: {raw})", p.source.as_str()) } - Some(_) => format!("\n Model source {}", p.source.as_str()), - None => format!("\n Model source {}", p.source.as_str()), + _ => format!("\n Model source {}", p.source.as_str()), }) .unwrap_or_default(); blocks.extend([ @@ -9016,26 +9027,24 @@ fn print_help(output_format: CliOutputFormat) -> Result<(), Box