use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
BinaryNotFound = 1,
SessionNotFound = 2,
PermissionDenied = 3,
McpError = 4,
ConfigError = 5,
InvalidInput = 6,
Timeout = 7,
SerializationError = 8,
IoError = 9,
ProcessError = 10,
StreamClosed = 11,
NotAuthenticated = 12,
RateLimitExceeded = 13,
Utf8Error = 14,
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "C{:03}", *self as u16)
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("[{code}] Claude Code not found in PATH", code = ErrorCode::BinaryNotFound)]
BinaryNotFound,
#[error("[{code}] Session {0} not found", code = ErrorCode::SessionNotFound)]
SessionNotFound(String),
#[error("[{code}] Tool permission denied: {0}", code = ErrorCode::PermissionDenied)]
PermissionDenied(String),
#[error("[{code}] MCP server error: {0}", code = ErrorCode::McpError)]
McpError(String),
#[error("[{code}] Invalid configuration: {0}", code = ErrorCode::ConfigError)]
ConfigError(String),
#[error("[{code}] Invalid input: {0}", code = ErrorCode::InvalidInput)]
InvalidInput(String),
#[error("[{code}] Operation timed out after {0}s", code = ErrorCode::Timeout)]
Timeout(u64),
#[error("[{code}] Serialization error: {0}", code = ErrorCode::SerializationError)]
SerializationError(#[from] serde_json::Error),
#[error("[{code}] IO error: {0}", code = ErrorCode::IoError)]
Io(#[from] std::io::Error),
#[error("[{code}] Process error: {0}", code = ErrorCode::ProcessError)]
ProcessError(String),
#[error("[{code}] Stream closed unexpectedly", code = ErrorCode::StreamClosed)]
StreamClosed,
#[error("[{code}] Claude CLI is not authenticated. Run 'claude auth' to authenticate.", code = ErrorCode::NotAuthenticated)]
NotAuthenticated,
#[error("[{code}] Rate limit exceeded. Please wait before retrying.", code = ErrorCode::RateLimitExceeded)]
RateLimitExceeded,
#[error("[{code}] UTF-8 conversion error: {0}", code = ErrorCode::Utf8Error)]
Utf8Error(#[from] std::string::FromUtf8Error),
}
impl Clone for Error {
fn clone(&self) -> Self {
match self {
Error::BinaryNotFound => Error::BinaryNotFound,
Error::SessionNotFound(s) => Error::SessionNotFound(s.clone()),
Error::PermissionDenied(s) => Error::PermissionDenied(s.clone()),
Error::McpError(s) => Error::McpError(s.clone()),
Error::ConfigError(s) => Error::ConfigError(s.clone()),
Error::InvalidInput(s) => Error::InvalidInput(s.clone()),
Error::Timeout(secs) => Error::Timeout(*secs),
Error::SerializationError(e) => Error::SerializationError(
serde_json::from_str::<serde_json::Value>(&format!("\"{}\"", e.to_string()))
.unwrap_err(),
),
Error::Io(e) => Error::Io(std::io::Error::new(e.kind(), e.to_string())),
Error::ProcessError(s) => Error::ProcessError(s.clone()),
Error::StreamClosed => Error::StreamClosed,
Error::NotAuthenticated => Error::NotAuthenticated,
Error::RateLimitExceeded => Error::RateLimitExceeded,
Error::Utf8Error(e) => {
Error::Utf8Error(std::string::String::from_utf8(e.as_bytes().to_vec()).unwrap_err())
}
}
}
}
impl Error {
pub fn code(&self) -> ErrorCode {
match self {
Error::BinaryNotFound => ErrorCode::BinaryNotFound,
Error::SessionNotFound(_) => ErrorCode::SessionNotFound,
Error::PermissionDenied(_) => ErrorCode::PermissionDenied,
Error::McpError(_) => ErrorCode::McpError,
Error::ConfigError(_) => ErrorCode::ConfigError,
Error::InvalidInput(_) => ErrorCode::InvalidInput,
Error::Timeout(_) => ErrorCode::Timeout,
Error::SerializationError(_) => ErrorCode::SerializationError,
Error::Io(_) => ErrorCode::IoError,
Error::ProcessError(_) => ErrorCode::ProcessError,
Error::StreamClosed => ErrorCode::StreamClosed,
Error::NotAuthenticated => ErrorCode::NotAuthenticated,
Error::RateLimitExceeded => ErrorCode::RateLimitExceeded,
Error::Utf8Error(_) => ErrorCode::Utf8Error,
}
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Error::Timeout(_)
| Error::RateLimitExceeded
| Error::StreamClosed
| Error::Io(_)
| Error::ProcessError(_)
)
}
}
#[cfg(feature = "cli")]
impl From<crate::cli::error::InteractiveError> for Error {
fn from(err: crate::cli::error::InteractiveError) -> Self {
match err {
crate::cli::error::InteractiveError::CommandDiscovery(msg) => Error::ProcessError(msg),
crate::cli::error::InteractiveError::CommandNotFound(msg) => {
Error::ProcessError(format!("Command not found: {}", msg))
}
crate::cli::error::InteractiveError::Session(msg) => Error::SessionNotFound(msg),
crate::cli::error::InteractiveError::SessionNotFound(id) => Error::SessionNotFound(id),
crate::cli::error::InteractiveError::Execution(msg) => Error::ProcessError(msg),
crate::cli::error::InteractiveError::ParallelExecution(msg) => {
Error::ProcessError(format!("Parallel execution: {}", msg))
}
crate::cli::error::InteractiveError::CostTracking(msg) => {
Error::ProcessError(format!("Cost tracking: {}", msg))
}
crate::cli::error::InteractiveError::History(msg) => {
Error::ProcessError(format!("History: {}", msg))
}
crate::cli::error::InteractiveError::OutputFormatting(msg) => {
Error::ProcessError(format!("Output formatting: {}", msg))
}
crate::cli::error::InteractiveError::Configuration(msg) => Error::ConfigError(msg),
crate::cli::error::InteractiveError::PermissionDenied(msg) => {
Error::PermissionDenied(msg)
}
crate::cli::error::InteractiveError::InvalidInput(msg) => Error::InvalidInput(msg),
crate::cli::error::InteractiveError::Timeout(secs) => Error::Timeout(secs),
crate::cli::error::InteractiveError::Io(err) => Error::Io(err),
crate::cli::error::InteractiveError::Serialization(err) => {
Error::SerializationError(err)
}
crate::cli::error::InteractiveError::ClaudeSDK(err) => err,
crate::cli::error::InteractiveError::Uuid(_) => {
Error::InvalidInput("UUID parsing error".to_string())
}
crate::cli::error::InteractiveError::FileWatcher(err) => Error::Io(
std::io::Error::new(std::io::ErrorKind::Other, err.to_string()),
),
crate::cli::error::InteractiveError::AsyncTask(err) => {
Error::ProcessError(format!("Async task: {}", err))
}
crate::cli::error::InteractiveError::Utf8Conversion(err) => Error::Utf8Error(err),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
assert_eq!(Error::BinaryNotFound.code(), ErrorCode::BinaryNotFound);
assert_eq!(
Error::SessionNotFound("test".to_string()).code(),
ErrorCode::SessionNotFound
);
assert_eq!(
Error::PermissionDenied("tool".to_string()).code(),
ErrorCode::PermissionDenied
);
assert_eq!(
Error::McpError("error".to_string()).code(),
ErrorCode::McpError
);
assert_eq!(
Error::ConfigError("invalid".to_string()).code(),
ErrorCode::ConfigError
);
assert_eq!(
Error::InvalidInput("bad".to_string()).code(),
ErrorCode::InvalidInput
);
assert_eq!(Error::Timeout(30).code(), ErrorCode::Timeout);
assert_eq!(
Error::ProcessError("failed".to_string()).code(),
ErrorCode::ProcessError
);
assert_eq!(Error::StreamClosed.code(), ErrorCode::StreamClosed);
assert_eq!(Error::NotAuthenticated.code(), ErrorCode::NotAuthenticated);
assert_eq!(
Error::RateLimitExceeded.code(),
ErrorCode::RateLimitExceeded
);
}
#[test]
fn test_error_code_display() {
assert_eq!(ErrorCode::BinaryNotFound.to_string(), "C001");
assert_eq!(ErrorCode::SessionNotFound.to_string(), "C002");
assert_eq!(ErrorCode::PermissionDenied.to_string(), "C003");
assert_eq!(ErrorCode::McpError.to_string(), "C004");
assert_eq!(ErrorCode::ConfigError.to_string(), "C005");
assert_eq!(ErrorCode::InvalidInput.to_string(), "C006");
assert_eq!(ErrorCode::Timeout.to_string(), "C007");
assert_eq!(ErrorCode::SerializationError.to_string(), "C008");
assert_eq!(ErrorCode::IoError.to_string(), "C009");
assert_eq!(ErrorCode::ProcessError.to_string(), "C010");
assert_eq!(ErrorCode::StreamClosed.to_string(), "C011");
assert_eq!(ErrorCode::NotAuthenticated.to_string(), "C012");
assert_eq!(ErrorCode::RateLimitExceeded.to_string(), "C013");
}
#[test]
fn test_error_messages_include_codes() {
let error = Error::BinaryNotFound;
assert!(error.to_string().contains("[C001]"));
let error = Error::Timeout(30);
assert!(error.to_string().contains("[C007]"));
assert!(error.to_string().contains("30s"));
let error = Error::NotAuthenticated;
assert!(error.to_string().contains("[C012]"));
assert!(error.to_string().contains("claude auth"));
}
#[test]
fn test_is_recoverable() {
assert!(Error::Timeout(30).is_recoverable());
assert!(Error::RateLimitExceeded.is_recoverable());
assert!(Error::StreamClosed.is_recoverable());
assert!(Error::ProcessError("temp failure".to_string()).is_recoverable());
assert!(!Error::BinaryNotFound.is_recoverable());
assert!(!Error::ConfigError("invalid".to_string()).is_recoverable());
assert!(!Error::InvalidInput("bad".to_string()).is_recoverable());
assert!(!Error::NotAuthenticated.is_recoverable());
assert!(!Error::PermissionDenied("denied".to_string()).is_recoverable());
assert!(
!Error::Utf8Error(std::string::String::from_utf8(vec![0xFF]).unwrap_err())
.is_recoverable()
);
}
#[test]
fn test_error_conversions() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let error: Error = io_error.into();
assert_eq!(error.code(), ErrorCode::IoError);
let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let error: Error = json_error.into();
assert_eq!(error.code(), ErrorCode::SerializationError);
}
#[test]
fn test_error_code_ordering() {
let codes = vec![
ErrorCode::BinaryNotFound as u16,
ErrorCode::SessionNotFound as u16,
ErrorCode::PermissionDenied as u16,
ErrorCode::McpError as u16,
ErrorCode::ConfigError as u16,
ErrorCode::InvalidInput as u16,
ErrorCode::Timeout as u16,
ErrorCode::SerializationError as u16,
ErrorCode::IoError as u16,
ErrorCode::ProcessError as u16,
ErrorCode::StreamClosed as u16,
ErrorCode::NotAuthenticated as u16,
ErrorCode::RateLimitExceeded as u16,
];
for i in 0..codes.len() {
assert_eq!(codes[i], (i + 1) as u16);
}
}
}