Better search paths and handling of CLI providers (#5554)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use rmcp::model::Role;
|
||||
use serde_json::{json, Value};
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
@@ -10,9 +11,12 @@ use tokio::process::Command;
|
||||
use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage};
|
||||
use super::errors::ProviderError;
|
||||
use super::utils::{filter_extensions_from_system_prompt, RequestLog};
|
||||
use crate::config::base::ClaudeCodeCommand;
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::subprocess::configure_command_no_window;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
pub const CLAUDE_CODE_DEFAULT_MODEL: &str = "claude-sonnet-4-20250514";
|
||||
@@ -21,7 +25,7 @@ pub const CLAUDE_CODE_DOC_URL: &str = "https://code.claude.com/docs/en/setup";
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct ClaudeCodeProvider {
|
||||
command: String,
|
||||
command: PathBuf,
|
||||
model: ModelConfig,
|
||||
#[serde(skip)]
|
||||
name: String,
|
||||
@@ -30,15 +34,8 @@ pub struct ClaudeCodeProvider {
|
||||
impl ClaudeCodeProvider {
|
||||
pub async fn from_env(model: ModelConfig) -> Result<Self> {
|
||||
let config = crate::config::Config::global();
|
||||
let command: String = config
|
||||
.get_param("CLAUDE_CODE_COMMAND")
|
||||
.unwrap_or_else(|_| "claude".to_string());
|
||||
|
||||
let resolved_command = if !command.contains('/') {
|
||||
Self::find_claude_executable(&command).unwrap_or(command)
|
||||
} else {
|
||||
command
|
||||
};
|
||||
let command: OsString = config.get_claude_code_command().unwrap_or_default().into();
|
||||
let resolved_command = SearchPaths::builder().with_npm().resolve(command)?;
|
||||
|
||||
Ok(Self {
|
||||
command: resolved_command,
|
||||
@@ -47,61 +44,6 @@ impl ClaudeCodeProvider {
|
||||
})
|
||||
}
|
||||
|
||||
/// Search for claude executable in common installation locations
|
||||
fn find_claude_executable(command_name: &str) -> Option<String> {
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
|
||||
let search_paths = vec![
|
||||
format!("{}/.claude/local/{}", home, command_name),
|
||||
format!("{}/.local/bin/{}", home, command_name),
|
||||
format!("{}/bin/{}", home, command_name),
|
||||
format!("/usr/local/bin/{}", command_name),
|
||||
format!("/usr/bin/{}", command_name),
|
||||
format!("/opt/claude/{}", command_name),
|
||||
];
|
||||
|
||||
for path in search_paths {
|
||||
let path_buf = PathBuf::from(&path);
|
||||
if path_buf.exists() && path_buf.is_file() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(metadata) = std::fs::metadata(&path_buf) {
|
||||
let permissions = metadata.permissions();
|
||||
if permissions.mode() & 0o111 != 0 {
|
||||
tracing::info!("Found claude executable at: {}", path);
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tracing::info!("Found claude executable at: {}", path);
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path_var) = std::env::var("PATH") {
|
||||
#[cfg(unix)]
|
||||
let path_separator = ':';
|
||||
#[cfg(windows)]
|
||||
let path_separator = ';';
|
||||
|
||||
for dir in path_var.split(path_separator) {
|
||||
let path_buf = PathBuf::from(dir).join(command_name);
|
||||
if path_buf.exists() && path_buf.is_file() {
|
||||
let full_path = path_buf.to_string_lossy().to_string();
|
||||
tracing::info!("Found claude executable in PATH at: {}", full_path);
|
||||
return Some(full_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::warn!("Could not find claude executable in common locations");
|
||||
None
|
||||
}
|
||||
|
||||
/// Convert goose messages to the format expected by claude CLI
|
||||
fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result<Value> {
|
||||
let mut claude_messages = Vec::new();
|
||||
@@ -312,7 +254,7 @@ impl ClaudeCodeProvider {
|
||||
|
||||
if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() {
|
||||
println!("=== CLAUDE CODE PROVIDER DEBUG ===");
|
||||
println!("Command: {}", self.command);
|
||||
println!("Command: {:?}", self.command);
|
||||
println!("Original system prompt length: {} chars", system.len());
|
||||
println!(
|
||||
"Filtered system prompt length: {} chars",
|
||||
@@ -328,6 +270,7 @@ impl ClaudeCodeProvider {
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&self.command);
|
||||
configure_command_no_window(&mut cmd);
|
||||
cmd.arg("-p")
|
||||
.arg(messages_json.to_string())
|
||||
.arg("--system-prompt")
|
||||
@@ -345,18 +288,12 @@ impl ClaudeCodeProvider {
|
||||
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| ProviderError::RequestFailed(format!(
|
||||
"\n\n ## Unable to find Claude Code CLI on the path.\n\n\
|
||||
**Error details:** Failed to spawn command '{}': {}\n\n\
|
||||
**Please ensure:**\n\
|
||||
- Claude Code CLI is installed and logged in\n\
|
||||
- The command is in your PATH, or set `CLAUDE_CODE_COMMAND` in your config\n\n\
|
||||
**For full features, please use the Anthropic provider with an API key if possible.**\n\n\
|
||||
Visit {} for installation instructions.",
|
||||
self.command, e, CLAUDE_CODE_DOC_URL
|
||||
)))?;
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
ProviderError::RequestFailed(format!(
|
||||
"Failed to spawn Claude CLI command '{:?}': {}.",
|
||||
self.command, e
|
||||
))
|
||||
})?;
|
||||
|
||||
let stdout = child
|
||||
.stdout
|
||||
@@ -461,12 +398,7 @@ impl Provider for ClaudeCodeProvider {
|
||||
CLAUDE_CODE_DEFAULT_MODEL,
|
||||
CLAUDE_CODE_KNOWN_MODELS.to_vec(),
|
||||
CLAUDE_CODE_DOC_URL,
|
||||
vec![ConfigKey::new(
|
||||
"CLAUDE_CODE_COMMAND",
|
||||
false,
|
||||
false,
|
||||
Some("claude"),
|
||||
)],
|
||||
vec![ConfigKey::from_value_type::<ClaudeCodeCommand>(true, false)],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -521,29 +453,3 @@ impl Provider for ClaudeCodeProvider {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ModelConfig;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_claude_code_invalid_model_no_fallback() {
|
||||
// Test that an invalid model is kept as-is (no fallback)
|
||||
let invalid_model = ModelConfig::new_or_fail("invalid-model");
|
||||
let provider = ClaudeCodeProvider::from_env(invalid_model).await.unwrap();
|
||||
let config = provider.get_model_config();
|
||||
|
||||
assert_eq!(config.model_name, "invalid-model");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_claude_code_valid_model() {
|
||||
// Test that a valid model is preserved
|
||||
let valid_model = ModelConfig::new_or_fail("sonnet");
|
||||
let provider = ClaudeCodeProvider::from_env(valid_model).await.unwrap();
|
||||
let config = provider.get_model_config();
|
||||
|
||||
assert_eq!(config.model_name, "sonnet");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user