1use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum SdkError {
12 #[error(
14 "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n\nSearched in:\n{searched_paths}"
15 )]
16 CliNotFound {
17 searched_paths: String,
19 },
20
21 #[error("Failed to connect to Claude CLI: {0}")]
23 ConnectionError(String),
24
25 #[error("Process error: {0}")]
27 ProcessError(#[from] std::io::Error),
28
29 #[error("Failed to parse message: {error}\nRaw message: {raw}")]
31 MessageParseError {
32 error: String,
34 raw: String,
36 },
37
38 #[error("JSON error: {0}")]
40 JsonError(#[from] serde_json::Error),
41
42 #[error("Failed to decode JSON from CLI output: {line}")]
44 CliJsonDecodeError {
45 line: String,
47 #[source]
49 original_error: serde_json::Error,
50 },
51
52 #[error("Transport error: {0}")]
54 TransportError(String),
55
56 #[error("Timeout waiting for response after {seconds} seconds")]
58 Timeout {
59 seconds: u64,
61 },
62
63 #[error("Session not found: {0}")]
65 SessionNotFound(String),
66
67 #[error("Invalid configuration: {0}")]
69 ConfigError(String),
70
71 #[error("Control request failed: {0}")]
73 ControlRequestError(String),
74
75 #[error("Unexpected response type: expected {expected}, got {actual}")]
77 UnexpectedResponse {
78 expected: String,
80 actual: String,
82 },
83
84 #[error("Claude CLI error: {message}")]
86 CliError {
87 message: String,
89 code: Option<String>,
91 },
92
93 #[error("Failed to send message through channel")]
95 ChannelSendError,
96
97 #[error("Channel closed unexpectedly")]
99 ChannelClosed,
100
101 #[error("Invalid state: {message}")]
103 InvalidState {
104 message: String,
106 },
107
108 #[error("Claude process exited unexpectedly with code {code:?}")]
110 ProcessExited {
111 code: Option<i32>,
113 },
114
115 #[error("Stream ended unexpectedly")]
117 UnexpectedStreamEnd,
118
119 #[error("Feature not supported: {feature}")]
121 NotSupported {
122 feature: String,
124 },
125}
126
127pub type Result<T> = std::result::Result<T, SdkError>;
129
130impl SdkError {
131 pub fn parse_error(error: impl Into<String>, raw: impl Into<String>) -> Self {
133 Self::MessageParseError {
134 error: error.into(),
135 raw: raw.into(),
136 }
137 }
138
139 pub fn timeout(seconds: u64) -> Self {
141 Self::Timeout { seconds }
142 }
143
144 pub fn unexpected_response(expected: impl Into<String>, actual: impl Into<String>) -> Self {
146 Self::UnexpectedResponse {
147 expected: expected.into(),
148 actual: actual.into(),
149 }
150 }
151
152 pub fn cli_error(message: impl Into<String>, code: Option<String>) -> Self {
154 Self::CliError {
155 message: message.into(),
156 code,
157 }
158 }
159
160 pub fn invalid_state(message: impl Into<String>) -> Self {
162 Self::InvalidState {
163 message: message.into(),
164 }
165 }
166
167 pub fn is_recoverable(&self) -> bool {
169 matches!(
170 self,
171 Self::Timeout { .. }
172 | Self::ChannelClosed
173 | Self::UnexpectedStreamEnd
174 | Self::ProcessExited { .. }
175 )
176 }
177
178 pub fn is_config_error(&self) -> bool {
180 matches!(
181 self,
182 Self::CliNotFound { .. } | Self::ConfigError(_) | Self::NotSupported { .. }
183 )
184 }
185}
186
187impl<T> From<tokio::sync::mpsc::error::SendError<T>> for SdkError {
189 fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
190 Self::ChannelSendError
191 }
192}
193
194impl From<tokio::sync::broadcast::error::RecvError> for SdkError {
195 fn from(_: tokio::sync::broadcast::error::RecvError) -> Self {
196 Self::ChannelClosed
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_error_display() {
206 let err = SdkError::CliNotFound {
207 searched_paths: "/usr/local/bin\n/usr/bin".to_string(),
208 };
209 let msg = err.to_string();
210 assert!(msg.contains("npm install -g @anthropic-ai/claude-code"));
211 assert!(msg.contains("/usr/local/bin"));
212 }
213
214 #[test]
215 fn test_is_recoverable() {
216 assert!(SdkError::timeout(30).is_recoverable());
217 assert!(SdkError::ChannelClosed.is_recoverable());
218 assert!(!SdkError::ConfigError("test".into()).is_recoverable());
219 }
220
221 #[test]
222 fn test_is_config_error() {
223 assert!(SdkError::ConfigError("test".into()).is_config_error());
224 assert!(
225 SdkError::CliNotFound {
226 searched_paths: "test".into()
227 }
228 .is_config_error()
229 );
230 assert!(!SdkError::timeout(30).is_config_error());
231 }
232
233 #[test]
234 fn test_cli_json_decode_error() {
235 let line = r#"{"invalid": json"#.to_string();
236 let original_err = serde_json::from_str::<serde_json::Value>(&line).unwrap_err();
237
238 let error = SdkError::CliJsonDecodeError {
239 line: line.clone(),
240 original_error: original_err,
241 };
242
243 let error_str = error.to_string();
244 assert!(error_str.contains("Failed to decode JSON from CLI output"));
245 assert!(error_str.contains(&line));
246 }
247}