1use std::collections::HashMap;
7use std::time::Duration;
8
9use reqwest::Method;
10use serde::{Deserialize, Serialize};
11
12use crate::client::{Client, EXEC_TIMEOUT_HEADROOM};
13use crate::error::BoxxkiteError;
14
15#[derive(Debug, Clone, Default)]
17pub struct ExecOptions {
18 timeout: Option<u32>,
19 description: Option<String>,
20}
21
22impl ExecOptions {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn timeout(mut self, timeout: u32) -> Self {
29 self.timeout = Some(timeout);
30 self
31 }
32
33 pub fn description(mut self, description: impl Into<String>) -> Self {
34 self.description = Some(description.into());
35 self
36 }
37}
38
39#[derive(Serialize)]
40struct ExecBody<'a> {
41 command: &'a str,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 timeout: Option<u32>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 description: Option<&'a str>,
46}
47
48#[derive(Debug, Clone, Deserialize)]
50pub struct ExecResult {
51 pub exit_code: i32,
52 pub stdout: String,
53 pub stderr: String,
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct HttpRequestOptions {
59 headers: Option<HashMap<String, String>>,
60 body: Option<String>,
61 timeout: Option<u32>,
62}
63
64impl HttpRequestOptions {
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
75 self.headers = Some(headers);
76 self
77 }
78
79 pub fn body(mut self, body: impl Into<String>) -> Self {
82 self.body = Some(body.into());
83 self
84 }
85
86 pub fn timeout(mut self, timeout: u32) -> Self {
88 self.timeout = Some(timeout);
89 self
90 }
91}
92
93#[derive(Serialize)]
94struct HttpRequestBody<'a> {
95 method: &'a str,
96 url: &'a str,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 headers: Option<&'a HashMap<String, String>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 body: Option<&'a str>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 timeout: Option<u32>,
103}
104
105#[derive(Debug, Clone, Deserialize)]
107pub struct HttpRequestResult {
108 pub status_code: i32,
109 #[serde(default)]
110 pub headers: HashMap<String, String>,
111 pub body: String,
112 #[serde(default)]
113 pub truncated: bool,
114}
115
116#[derive(Debug, Clone, Default)]
119pub struct FileOptions {
120 description: Option<String>,
121}
122
123impl FileOptions {
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn description(mut self, description: impl Into<String>) -> Self {
129 self.description = Some(description.into());
130 self
131 }
132}
133
134#[derive(Serialize)]
135struct FileCreateBody<'a> {
136 path: &'a str,
137 content: &'a str,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 description: Option<&'a str>,
140}
141
142#[derive(Debug, Clone, Deserialize)]
144pub struct FileCreateResult {
145 pub path: String,
146 pub size: i64,
147 pub created: bool,
148}
149
150#[derive(Debug, Clone, Default)]
152pub struct ViewOptions {
153 view_range: Option<[i64; 2]>,
154 description: Option<String>,
155}
156
157impl ViewOptions {
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn view_range(mut self, start_line: i64, end_line: i64) -> Self {
164 self.view_range = Some([start_line, end_line]);
165 self
166 }
167
168 pub fn description(mut self, description: impl Into<String>) -> Self {
169 self.description = Some(description.into());
170 self
171 }
172}
173
174#[derive(Serialize)]
175struct ViewBody<'a> {
176 path: &'a str,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 view_range: Option<[i64; 2]>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 description: Option<&'a str>,
181}
182
183#[derive(Debug, Clone, Deserialize)]
185pub struct ViewResult {
186 pub content: String,
187 pub lines: i64,
188 #[serde(default)]
189 pub is_directory: bool,
190 pub entries: Option<Vec<String>>,
191}
192
193#[derive(Debug, Clone, Default)]
195pub struct StrReplaceOptions {
196 replace_all: bool,
197 description: Option<String>,
198}
199
200impl StrReplaceOptions {
201 pub fn new() -> Self {
202 Self::default()
203 }
204
205 pub fn replace_all(mut self, replace_all: bool) -> Self {
208 self.replace_all = replace_all;
209 self
210 }
211
212 pub fn description(mut self, description: impl Into<String>) -> Self {
213 self.description = Some(description.into());
214 self
215 }
216}
217
218#[derive(Serialize)]
219struct StrReplaceBody<'a> {
220 path: &'a str,
221 old_str: &'a str,
222 new_str: &'a str,
223 replace_all: bool,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 description: Option<&'a str>,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct StrReplaceResult {
231 pub path: String,
232 pub replaced: bool,
233 pub occurrences: i64,
234}
235
236#[derive(Debug, Clone, Default)]
238pub struct LsOptions {
239 path: Option<String>,
240}
241
242impl LsOptions {
243 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn path(mut self, path: impl Into<String>) -> Self {
249 self.path = Some(path.into());
250 self
251 }
252}
253
254#[derive(Serialize)]
255struct LsBody {
256 path: String,
257}
258
259#[derive(Debug, Clone, Deserialize)]
263pub struct LsResult {
264 pub entries: Vec<serde_json::Value>,
265}
266
267#[derive(Debug, Clone, Default)]
269pub struct GlobOptions {
270 path: Option<String>,
271}
272
273impl GlobOptions {
274 pub fn new() -> Self {
275 Self::default()
276 }
277
278 pub fn path(mut self, path: impl Into<String>) -> Self {
280 self.path = Some(path.into());
281 self
282 }
283}
284
285#[derive(Serialize)]
286struct GlobBody<'a> {
287 pattern: &'a str,
288 path: String,
289}
290
291#[derive(Debug, Clone, Deserialize)]
293pub struct GlobResult {
294 pub matches: Vec<serde_json::Value>,
295}
296
297#[derive(Debug, Clone, Default)]
299pub struct GrepOptions {
300 path: Option<String>,
301 glob: Option<String>,
302 max_matches: Option<u32>,
303}
304
305impl GrepOptions {
306 pub fn new() -> Self {
307 Self::default()
308 }
309
310 pub fn path(mut self, path: impl Into<String>) -> Self {
312 self.path = Some(path.into());
313 self
314 }
315
316 pub fn glob(mut self, glob: impl Into<String>) -> Self {
318 self.glob = Some(glob.into());
319 self
320 }
321
322 pub fn max_matches(mut self, max_matches: u32) -> Self {
324 self.max_matches = Some(max_matches);
325 self
326 }
327}
328
329#[derive(Serialize)]
330struct GrepBody<'a> {
331 pattern: &'a str,
332 path: String,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 glob: Option<&'a str>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 max_matches: Option<u32>,
337}
338
339#[derive(Debug, Clone, Deserialize)]
341pub struct GrepResult {
342 pub matches: Vec<serde_json::Value>,
343 pub error: Option<String>,
344 #[serde(default)]
345 pub truncated: bool,
346}
347
348impl Client {
349 pub async fn exec(
353 &self,
354 session_id: &str,
355 command: &str,
356 options: ExecOptions,
357 ) -> Result<ExecResult, BoxxkiteError> {
358 let body = ExecBody {
359 command,
360 timeout: options.timeout,
361 description: options.description.as_deref(),
362 };
363 let mut builder = self
364 .request(Method::POST, &format!("/v1/sandboxes/{session_id}/exec"))
365 .json(&body);
366 if let Some(timeout) = options.timeout {
367 builder = builder.timeout(Duration::from_secs(timeout as u64) + EXEC_TIMEOUT_HEADROOM);
368 }
369 self.send(builder).await
370 }
371
372 pub async fn http_request(
380 &self,
381 session_id: &str,
382 method: &str,
383 url: &str,
384 options: HttpRequestOptions,
385 ) -> Result<HttpRequestResult, BoxxkiteError> {
386 let body = HttpRequestBody {
387 method,
388 url,
389 headers: options.headers.as_ref(),
390 body: options.body.as_deref(),
391 timeout: options.timeout,
392 };
393 let mut builder = self
394 .request(
395 Method::POST,
396 &format!("/v1/sandboxes/{session_id}/http-request"),
397 )
398 .json(&body);
399 if let Some(timeout) = options.timeout {
400 builder = builder.timeout(Duration::from_secs(timeout as u64) + EXEC_TIMEOUT_HEADROOM);
401 }
402 self.send(builder).await
403 }
404
405 pub async fn file_create(
408 &self,
409 session_id: &str,
410 path: &str,
411 content: &str,
412 options: FileOptions,
413 ) -> Result<FileCreateResult, BoxxkiteError> {
414 let body = FileCreateBody {
415 path,
416 content,
417 description: options.description.as_deref(),
418 };
419 let builder = self
420 .request(Method::POST, &format!("/v1/sandboxes/{session_id}/files"))
421 .json(&body);
422 self.send(builder).await
423 }
424
425 pub async fn view(
430 &self,
431 session_id: &str,
432 path: &str,
433 options: ViewOptions,
434 ) -> Result<ViewResult, BoxxkiteError> {
435 let body = ViewBody {
436 path,
437 view_range: options.view_range,
438 description: options.description.as_deref(),
439 };
440 let builder = self
441 .request(
442 Method::POST,
443 &format!("/v1/sandboxes/{session_id}/files/view"),
444 )
445 .json(&body);
446 self.send(builder).await
447 }
448
449 pub async fn str_replace(
453 &self,
454 session_id: &str,
455 path: &str,
456 old_str: &str,
457 new_str: &str,
458 options: StrReplaceOptions,
459 ) -> Result<StrReplaceResult, BoxxkiteError> {
460 let body = StrReplaceBody {
461 path,
462 old_str,
463 new_str,
464 replace_all: options.replace_all,
465 description: options.description.as_deref(),
466 };
467 let builder = self
468 .request(
469 Method::POST,
470 &format!("/v1/sandboxes/{session_id}/files/str-replace"),
471 )
472 .json(&body);
473 self.send(builder).await
474 }
475
476 pub async fn ls(&self, session_id: &str, options: LsOptions) -> Result<LsResult, BoxxkiteError> {
478 let body = LsBody {
479 path: options.path.unwrap_or_else(|| "/".to_string()),
480 };
481 let builder = self
482 .request(
483 Method::POST,
484 &format!("/v1/sandboxes/{session_id}/files/ls"),
485 )
486 .json(&body);
487 self.send(builder).await
488 }
489
490 pub async fn glob(
493 &self,
494 session_id: &str,
495 pattern: &str,
496 options: GlobOptions,
497 ) -> Result<GlobResult, BoxxkiteError> {
498 let body = GlobBody {
499 pattern,
500 path: options.path.unwrap_or_else(|| "/".to_string()),
501 };
502 let builder = self
503 .request(
504 Method::POST,
505 &format!("/v1/sandboxes/{session_id}/files/glob"),
506 )
507 .json(&body);
508 self.send(builder).await
509 }
510
511 pub async fn grep(
513 &self,
514 session_id: &str,
515 pattern: &str,
516 options: GrepOptions,
517 ) -> Result<GrepResult, BoxxkiteError> {
518 let body = GrepBody {
519 pattern,
520 path: options.path.unwrap_or_else(|| "/".to_string()),
521 glob: options.glob.as_deref(),
522 max_matches: options.max_matches,
523 };
524 let builder = self
525 .request(
526 Method::POST,
527 &format!("/v1/sandboxes/{session_id}/files/grep"),
528 )
529 .json(&body);
530 self.send(builder).await
531 }
532}