1use thiserror::Error;
8
9#[derive(Error, Debug)]
11#[non_exhaustive]
12pub enum SdkError {
13 #[error(
15 "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n\nSearched in:\n{searched_paths}"
16 )]
17 CliNotFound {
18 searched_paths: String,
20 },
21
22 #[error("Failed to connect to Claude CLI: {0}")]
24 ConnectionError(String),
25
26 #[error("Process error: {0}")]
28 ProcessError(#[from] std::io::Error),
29
30 #[error("Failed to parse message: {error}\nRaw message: {raw}")]
32 MessageParseError {
33 error: String,
35 raw: String,
37 },
38
39 #[error("JSON error: {0}")]
41 JsonError(#[from] serde_json::Error),
42
43 #[error("Failed to decode JSON from CLI output: {line}")]
45 CliJsonDecodeError {
46 line: String,
48 #[source]
50 original_error: serde_json::Error,
51 },
52
53 #[error("Transport error: {0}")]
55 TransportError(String),
56
57 #[error("Timeout waiting for response after {seconds} seconds")]
59 Timeout {
60 seconds: u64,
62 },
63
64 #[error("Session not found: {0}")]
66 SessionNotFound(String),
67
68 #[error("Invalid configuration: {0}")]
70 ConfigError(String),
71
72 #[error("Control request failed: {0}")]
74 ControlRequestError(String),
75
76 #[error("Unexpected response type: expected {expected}, got {actual}")]
78 UnexpectedResponse {
79 expected: String,
81 actual: String,
83 },
84
85 #[error("Claude CLI error: {message}")]
87 CliError {
88 message: String,
90 code: Option<String>,
92 },
93
94 #[error("Failed to send message through channel")]
96 ChannelSendError,
97
98 #[error("Channel closed unexpectedly")]
100 ChannelClosed,
101
102 #[error("Invalid state: {message}")]
104 InvalidState {
105 message: String,
107 },
108
109 #[error("Claude process exited unexpectedly with code {code:?}")]
111 ProcessExited {
112 code: Option<i32>,
114 },
115
116 #[error("Stream ended unexpectedly")]
118 UnexpectedStreamEnd,
119
120 #[error("Feature not supported: {feature}")]
122 NotSupported {
123 feature: String,
125 },
126
127 #[cfg(feature = "websocket")]
129 #[error("WebSocket error: {0}")]
130 WebSocketError(String),
131}
132
133pub type Result<T> = std::result::Result<T, SdkError>;
135
136impl SdkError {
137 pub fn parse_error(error: impl Into<String>, raw: impl Into<String>) -> Self {
139 Self::MessageParseError {
140 error: error.into(),
141 raw: raw.into(),
142 }
143 }
144
145 pub fn timeout(seconds: u64) -> Self {
147 Self::Timeout { seconds }
148 }
149
150 pub fn unexpected_response(expected: impl Into<String>, actual: impl Into<String>) -> Self {
152 Self::UnexpectedResponse {
153 expected: expected.into(),
154 actual: actual.into(),
155 }
156 }
157
158 pub fn cli_error(message: impl Into<String>, code: Option<String>) -> Self {
160 Self::CliError {
161 message: message.into(),
162 code,
163 }
164 }
165
166 pub fn invalid_state(message: impl Into<String>) -> Self {
168 Self::InvalidState {
169 message: message.into(),
170 }
171 }
172
173 pub fn is_recoverable(&self) -> bool {
175 matches!(
176 self,
177 Self::Timeout { .. }
178 | Self::ChannelClosed
179 | Self::UnexpectedStreamEnd
180 | Self::ProcessExited { .. }
181 )
182 }
183
184 pub fn is_config_error(&self) -> bool {
186 matches!(
187 self,
188 Self::CliNotFound { .. } | Self::ConfigError(_) | Self::NotSupported { .. }
189 )
190 }
191}
192
193impl<T> From<tokio::sync::mpsc::error::SendError<T>> for SdkError {
195 fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
196 Self::ChannelSendError
197 }
198}
199
200impl From<tokio::sync::broadcast::error::RecvError> for SdkError {
201 fn from(_: tokio::sync::broadcast::error::RecvError) -> Self {
202 Self::ChannelClosed
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_error_display() {
212 let err = SdkError::CliNotFound {
213 searched_paths: "/usr/local/bin\n/usr/bin".to_string(),
214 };
215 let msg = err.to_string();
216 assert!(msg.contains("npm install -g @anthropic-ai/claude-code"));
217 assert!(msg.contains("/usr/local/bin"));
218 }
219
220 #[test]
221 fn test_is_recoverable() {
222 assert!(SdkError::timeout(30).is_recoverable());
223 assert!(SdkError::ChannelClosed.is_recoverable());
224 assert!(!SdkError::ConfigError("test".into()).is_recoverable());
225 }
226
227 #[test]
228 fn test_is_config_error() {
229 assert!(SdkError::ConfigError("test".into()).is_config_error());
230 assert!(
231 SdkError::CliNotFound {
232 searched_paths: "test".into()
233 }
234 .is_config_error()
235 );
236 assert!(!SdkError::timeout(30).is_config_error());
237 }
238
239 #[test]
240 fn test_cli_json_decode_error() {
241 let line = r#"{"invalid": json"#.to_string();
242 let original_err = serde_json::from_str::<serde_json::Value>(&line).unwrap_err();
243
244 let error = SdkError::CliJsonDecodeError {
245 line: line.clone(),
246 original_error: original_err,
247 };
248
249 let error_str = error.to_string();
250 assert!(error_str.contains("Failed to decode JSON from CLI output"));
251 assert!(error_str.contains(&line));
252 }
253}