use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum SdkError {
#[error(
"Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n\nSearched in:\n{searched_paths}"
)]
CliNotFound {
searched_paths: String,
},
#[error("Failed to connect to Claude CLI: {0}")]
ConnectionError(String),
#[error("Process error: {0}")]
ProcessError(#[from] std::io::Error),
#[error("Failed to parse message: {error}\nRaw message: {raw}")]
MessageParseError {
error: String,
raw: String,
},
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Failed to decode JSON from CLI output: {line}")]
CliJsonDecodeError {
line: String,
#[source]
original_error: serde_json::Error,
},
#[error("Transport error: {0}")]
TransportError(String),
#[error("Timeout waiting for response after {seconds} seconds")]
Timeout {
seconds: u64,
},
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Invalid configuration: {0}")]
ConfigError(String),
#[error("Control request failed: {0}")]
ControlRequestError(String),
#[error("Unexpected response type: expected {expected}, got {actual}")]
UnexpectedResponse {
expected: String,
actual: String,
},
#[error("Claude CLI error: {message}")]
CliError {
message: String,
code: Option<String>,
},
#[error("Failed to send message through channel")]
ChannelSendError,
#[error("Channel closed unexpectedly")]
ChannelClosed,
#[error("Invalid state: {message}")]
InvalidState {
message: String,
},
#[error("Claude process exited unexpectedly with code {code:?}")]
ProcessExited {
code: Option<i32>,
},
#[error("Stream ended unexpectedly")]
UnexpectedStreamEnd,
#[error("Feature not supported: {feature}")]
NotSupported {
feature: String,
},
#[cfg(feature = "websocket")]
#[error("WebSocket error: {0}")]
WebSocketError(String),
}
pub type Result<T> = std::result::Result<T, SdkError>;
impl SdkError {
pub fn parse_error(error: impl Into<String>, raw: impl Into<String>) -> Self {
Self::MessageParseError {
error: error.into(),
raw: raw.into(),
}
}
pub fn timeout(seconds: u64) -> Self {
Self::Timeout { seconds }
}
pub fn unexpected_response(expected: impl Into<String>, actual: impl Into<String>) -> Self {
Self::UnexpectedResponse {
expected: expected.into(),
actual: actual.into(),
}
}
pub fn cli_error(message: impl Into<String>, code: Option<String>) -> Self {
Self::CliError {
message: message.into(),
code,
}
}
pub fn invalid_state(message: impl Into<String>) -> Self {
Self::InvalidState {
message: message.into(),
}
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::Timeout { .. }
| Self::ChannelClosed
| Self::UnexpectedStreamEnd
| Self::ProcessExited { .. }
)
}
pub fn is_config_error(&self) -> bool {
matches!(
self,
Self::CliNotFound { .. } | Self::ConfigError(_) | Self::NotSupported { .. }
)
}
}
impl<T> From<tokio::sync::mpsc::error::SendError<T>> for SdkError {
fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
Self::ChannelSendError
}
}
impl From<tokio::sync::broadcast::error::RecvError> for SdkError {
fn from(_: tokio::sync::broadcast::error::RecvError) -> Self {
Self::ChannelClosed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = SdkError::CliNotFound {
searched_paths: "/usr/local/bin\n/usr/bin".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("npm install -g @anthropic-ai/claude-code"));
assert!(msg.contains("/usr/local/bin"));
}
#[test]
fn test_is_recoverable() {
assert!(SdkError::timeout(30).is_recoverable());
assert!(SdkError::ChannelClosed.is_recoverable());
assert!(!SdkError::ConfigError("test".into()).is_recoverable());
}
#[test]
fn test_is_config_error() {
assert!(SdkError::ConfigError("test".into()).is_config_error());
assert!(
SdkError::CliNotFound {
searched_paths: "test".into()
}
.is_config_error()
);
assert!(!SdkError::timeout(30).is_config_error());
}
#[test]
fn test_cli_json_decode_error() {
let line = r#"{"invalid": json"#.to_string();
let original_err = serde_json::from_str::<serde_json::Value>(&line).unwrap_err();
let error = SdkError::CliJsonDecodeError {
line: line.clone(),
original_error: original_err,
};
let error_str = error.to_string();
assert!(error_str.contains("Failed to decode JSON from CLI output"));
assert!(error_str.contains(&line));
}
}