1use std::collections::HashMap;
9use std::fmt;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::time::Duration;
13
14use async_stream::try_stream;
15use futures::{Stream, StreamExt};
16use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
17use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
18use reqwest::{Method, Url};
19use serde_json::Value;
20
21use crate::errors::{ApiError, Error, OpencodeSDKError, Result};
22
23const COMPONENT_ENCODE_SET: &AsciiSet = &CONTROLS
25 .add(b' ')
26 .add(b'"')
27 .add(b'#')
28 .add(b'$')
29 .add(b'%')
30 .add(b'&')
31 .add(b'+')
32 .add(b',')
33 .add(b'/')
34 .add(b':')
35 .add(b';')
36 .add(b'<')
37 .add(b'=')
38 .add(b'>')
39 .add(b'?')
40 .add(b'@')
41 .add(b'[')
42 .add(b'\\')
43 .add(b']')
44 .add(b'^')
45 .add(b'`')
46 .add(b'{')
47 .add(b'|')
48 .add(b'}');
49
50#[derive(Debug, Clone, Default)]
52pub struct RequestOptions {
53 pub path: HashMap<String, String>,
55 pub query: HashMap<String, Value>,
57 pub headers: HashMap<String, String>,
59 pub body: Option<Value>,
61}
62
63impl RequestOptions {
64 pub fn with_path(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
68 self.path.insert(key.into(), value.into());
69 self
70 }
71
72 pub fn with_query(mut self, key: impl Into<String>, value: Value) -> Self {
74 self.query.insert(key.into(), value);
75 self
76 }
77
78 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80 self.headers.insert(key.into(), value.into());
81 self
82 }
83
84 pub fn with_body(mut self, value: Value) -> Self {
86 self.body = Some(value);
87 self
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct ApiResponse {
94 pub data: Value,
96 pub status: u16,
98 pub headers: HashMap<String, String>,
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Eq)]
104pub struct SseEvent {
105 pub event: Option<String>,
107 pub id: Option<String>,
109 pub retry: Option<u64>,
111 pub data: String,
113}
114
115impl SseEvent {
116 fn is_empty(&self) -> bool {
117 self.event.is_none() && self.id.is_none() && self.retry.is_none() && self.data.is_empty()
118 }
119}
120
121pub type SseStream = Pin<Box<dyn Stream<Item = Result<SseEvent>> + Send>>;
123
124#[derive(Clone)]
126pub struct OpencodeClientConfig {
127 pub base_url: String,
129 pub directory: Option<String>,
131 pub headers: HashMap<String, String>,
133 pub bearer_token: Option<String>,
135 pub timeout: Duration,
137}
138
139impl fmt::Debug for OpencodeClientConfig {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.debug_struct("OpencodeClientConfig")
142 .field("base_url", &self.base_url)
143 .field("directory", &self.directory)
144 .field("headers", &"<redacted>")
145 .field(
146 "bearer_token",
147 &self.bearer_token.as_ref().map(|_| "<redacted>"),
148 )
149 .field("timeout", &self.timeout)
150 .finish()
151 }
152}
153
154impl Default for OpencodeClientConfig {
155 fn default() -> Self {
156 Self {
157 base_url: "http://127.0.0.1:4096".to_string(),
158 directory: None,
159 headers: HashMap::new(),
160 bearer_token: None,
161 timeout: Duration::from_secs(60),
162 }
163 }
164}
165
166struct ClientInner {
167 http: reqwest::Client,
168 base_url: String,
169 default_headers: HeaderMap,
170}
171
172impl fmt::Debug for ClientInner {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.debug_struct("ClientInner")
175 .field("base_url", &self.base_url)
176 .field("default_headers", &"<redacted>")
177 .finish()
178 }
179}
180
181#[derive(Clone)]
183pub struct OpencodeClient {
184 inner: Arc<ClientInner>,
185}
186
187impl fmt::Debug for OpencodeClient {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.debug_struct("OpencodeClient")
190 .field("base_url", &self.inner.base_url)
191 .finish()
192 }
193}
194
195pub fn create_opencode_client(config: Option<OpencodeClientConfig>) -> Result<OpencodeClient> {
203 let config = config.unwrap_or_default();
204
205 let mut default_headers = HeaderMap::new();
206 for (k, v) in &config.headers {
207 let name = HeaderName::from_bytes(k.as_bytes())?;
208 let value = HeaderValue::from_str(v)?;
209 default_headers.insert(name, value);
210 }
211
212 if let Some(directory) = &config.directory {
213 let encoded = encode_directory_header(directory);
214 default_headers.insert(
215 HeaderName::from_static("x-opencode-directory"),
216 HeaderValue::from_str(&encoded)?,
217 );
218 }
219
220 if let Some(token) = &config.bearer_token {
221 default_headers.insert(
222 HeaderName::from_static("authorization"),
223 HeaderValue::from_str(&format!("Bearer {token}"))?,
224 );
225 }
226
227 let http = reqwest::Client::builder()
228 .timeout(config.timeout)
229 .default_headers(default_headers.clone())
230 .build()?;
231
232 Ok(OpencodeClient {
233 inner: Arc::new(ClientInner {
234 http,
235 base_url: config.base_url,
236 default_headers,
237 }),
238 })
239}
240
241impl OpencodeClient {
242 pub fn session(&self) -> SessionApi {
244 SessionApi {
245 client: self.clone(),
246 }
247 }
248
249 pub fn app(&self) -> AppApi {
251 AppApi {
252 client: self.clone(),
253 }
254 }
255
256 pub fn global(&self) -> GlobalApi {
258 GlobalApi {
259 client: self.clone(),
260 }
261 }
262
263 pub fn command(&self) -> CommandApi {
265 CommandApi {
266 client: self.clone(),
267 }
268 }
269
270 pub fn config(&self) -> ConfigApi {
272 ConfigApi {
273 client: self.clone(),
274 }
275 }
276
277 pub fn project(&self) -> ProjectApi {
279 ProjectApi {
280 client: self.clone(),
281 }
282 }
283
284 pub fn path(&self) -> PathApi {
286 PathApi {
287 client: self.clone(),
288 }
289 }
290
291 pub fn file(&self) -> FileApi {
293 FileApi {
294 client: self.clone(),
295 }
296 }
297
298 pub fn lsp(&self) -> LspApi {
300 LspApi {
301 client: self.clone(),
302 }
303 }
304
305 pub fn tool(&self) -> ToolApi {
307 ToolApi {
308 client: self.clone(),
309 }
310 }
311
312 pub fn provider(&self) -> ProviderApi {
314 ProviderApi {
315 client: self.clone(),
316 }
317 }
318
319 pub fn auth(&self) -> AuthApi {
321 AuthApi {
322 client: self.clone(),
323 }
324 }
325
326 pub fn mcp(&self) -> McpApi {
328 McpApi {
329 client: self.clone(),
330 }
331 }
332
333 pub fn pty(&self) -> PtyApi {
335 PtyApi {
336 client: self.clone(),
337 }
338 }
339
340 pub fn event(&self) -> EventApi {
342 EventApi {
343 client: self.clone(),
344 }
345 }
346
347 pub fn formatter(&self) -> FormatterApi {
349 FormatterApi {
350 client: self.clone(),
351 }
352 }
353
354 pub fn find(&self) -> FindApi {
356 FindApi {
357 client: self.clone(),
358 }
359 }
360
361 pub fn instance(&self) -> InstanceApi {
363 InstanceApi {
364 client: self.clone(),
365 }
366 }
367
368 pub fn vcs(&self) -> VcsApi {
370 VcsApi {
371 client: self.clone(),
372 }
373 }
374
375 pub fn tui(&self) -> TuiApi {
377 TuiApi {
378 client: self.clone(),
379 }
380 }
381
382 pub fn control(&self) -> ControlApi {
384 ControlApi {
385 client: self.clone(),
386 }
387 }
388
389 pub async fn call_operation(
391 &self,
392 operation_id: &str,
393 options: RequestOptions,
394 ) -> Result<ApiResponse> {
395 let (method, path, is_sse) = operation_spec(operation_id).ok_or_else(|| {
396 Error::OpencodeSDK(OpencodeSDKError::new(format!(
397 "Unknown operation id: {operation_id}"
398 )))
399 })?;
400
401 if is_sse {
402 return Err(Error::OpencodeSDK(OpencodeSDKError::new(format!(
403 "Operation {operation_id} is SSE; use call_operation_sse"
404 ))));
405 }
406
407 self.request_json(method, path, options).await
408 }
409
410 pub async fn call_operation_sse(
412 &self,
413 operation_id: &str,
414 options: RequestOptions,
415 ) -> Result<SseStream> {
416 let (method, path, is_sse) = operation_spec(operation_id).ok_or_else(|| {
417 Error::OpencodeSDK(OpencodeSDKError::new(format!(
418 "Unknown operation id: {operation_id}"
419 )))
420 })?;
421
422 if !is_sse {
423 return Err(Error::OpencodeSDK(OpencodeSDKError::new(format!(
424 "Operation {operation_id} is not SSE; use call_operation"
425 ))));
426 }
427
428 self.request_sse(method, path, options).await
429 }
430
431 pub async fn request_json(
440 &self,
441 method: Method,
442 path_template: &str,
443 options: RequestOptions,
444 ) -> Result<ApiResponse> {
445 let response = self.send_request(method, path_template, options).await?;
446 let status = response.status().as_u16();
447 let headers = headers_to_map(response.headers());
448 let bytes = response.bytes().await?;
449
450 if (200..300).contains(&status) {
451 let data = if status == 204 || bytes.is_empty() {
452 serde_json::json!({})
453 } else {
454 parse_success_body(&bytes)
455 };
456
457 return Ok(ApiResponse {
458 data,
459 status,
460 headers,
461 });
462 }
463
464 let body_text = String::from_utf8_lossy(&bytes).to_string();
465 Err(Error::Api(ApiError {
466 status,
467 body: body_text,
468 }))
469 }
470
471 pub async fn request_sse(
478 &self,
479 method: Method,
480 path_template: &str,
481 options: RequestOptions,
482 ) -> Result<SseStream> {
483 let response = self.send_request(method, path_template, options).await?;
484 let status = response.status().as_u16();
485
486 if !(200..300).contains(&status) {
487 let body_text = response.text().await.unwrap_or_default();
488 return Err(Error::Api(ApiError {
489 status,
490 body: body_text,
491 }));
492 }
493
494 let byte_stream = response.bytes_stream();
495 let out = try_stream! {
496 let mut buffer = Vec::<u8>::new();
497 let mut current = SseEvent::default();
498
499 futures::pin_mut!(byte_stream);
500 while let Some(chunk) = byte_stream.next().await {
501 let chunk = chunk?;
502 buffer.extend_from_slice(&chunk);
503
504 while let Some(newline_idx) = buffer.iter().position(|b| *b == b'\n') {
505 let mut line = buffer.drain(..=newline_idx).collect::<Vec<_>>();
506 if matches!(line.last(), Some(b'\n')) {
507 line.pop();
508 }
509 if matches!(line.last(), Some(b'\r')) {
510 line.pop();
511 }
512
513 let line = String::from_utf8_lossy(&line).into_owned();
514
515 if line.is_empty() {
516 if !current.is_empty() {
517 let emitted = std::mem::take(&mut current);
518 yield emitted;
519 }
520 continue;
521 }
522
523 apply_sse_line(&line, &mut current);
524 }
525 }
526
527 if !buffer.is_empty() {
528 if matches!(buffer.last(), Some(b'\r')) {
529 buffer.pop();
530 }
531 let line = String::from_utf8_lossy(&buffer).into_owned();
532 if !line.is_empty() {
533 apply_sse_line(&line, &mut current);
534 }
535 }
536
537 if !current.is_empty() {
538 yield current;
539 }
540 };
541
542 Ok(Box::pin(out))
543 }
544
545 async fn send_request(
546 &self,
547 method: Method,
548 path_template: &str,
549 options: RequestOptions,
550 ) -> Result<reqwest::Response> {
551 let url = self.build_url(path_template, &options.path, &options.query)?;
552
553 let mut req = self.inner.http.request(method, url);
554
555 let mut merged_headers = self.inner.default_headers.clone();
556 for (k, v) in &options.headers {
557 let name = HeaderName::from_bytes(k.as_bytes()).map_err(|e| {
558 Error::OpencodeSDK(OpencodeSDKError::new(format!(
559 "Invalid header name {k}: {e}"
560 )))
561 })?;
562 let value = HeaderValue::from_str(v)?;
563 merged_headers.insert(name, value);
564 }
565 req = req.headers(merged_headers);
566
567 if let Some(body) = options.body {
568 req = req.json(&body);
569 }
570
571 Ok(req.send().await?)
572 }
573
574 fn build_url(
575 &self,
576 path_template: &str,
577 path_params: &HashMap<String, String>,
578 query_params: &HashMap<String, Value>,
579 ) -> Result<Url> {
580 let allow_id_fallback = path_template.matches('{').count() == 1;
581 let mut rendered_path = String::new();
582 let mut chars = path_template.chars().peekable();
583
584 while let Some(ch) = chars.next() {
585 if ch != '{' {
586 rendered_path.push(ch);
587 continue;
588 }
589
590 let mut key = String::new();
591 for next in chars.by_ref() {
592 if next == '}' {
593 break;
594 }
595 key.push(next);
596 }
597
598 if key.is_empty() {
599 return Err(Error::OpencodeSDK(OpencodeSDKError::new(
600 "Empty path parameter name in template",
601 )));
602 }
603
604 let value = resolve_path_value(path_params, &key, allow_id_fallback)
605 .ok_or_else(|| Error::MissingPathParameter(key.clone()))?;
606 rendered_path.push_str(&encode_component(value));
607 }
608
609 let base = self.inner.base_url.trim_end_matches('/');
610 let suffix = rendered_path.trim_start_matches('/');
611 let full = format!("{base}/{suffix}");
612
613 let mut url = Url::parse(&full).map_err(|e| {
614 Error::OpencodeSDK(OpencodeSDKError::new(format!("Invalid URL {full}: {e}")))
615 })?;
616
617 let mut pairs = Vec::new();
618 for (key, value) in query_params {
619 append_query_value(&mut pairs, key, value);
620 }
621 if !pairs.is_empty() {
622 let mut qp = url.query_pairs_mut();
623 for (key, value) in pairs {
624 qp.append_pair(&key, &value);
625 }
626 }
627
628 Ok(url)
629 }
630}
631
632#[derive(Debug, Clone)]
634pub struct FindApi {
635 client: OpencodeClient,
636}
637
638impl FindApi {
639 pub async fn text(&self, options: RequestOptions) -> Result<ApiResponse> {
641 self.client
642 .request_json(Method::GET, "/find", options)
643 .await
644 }
645
646 pub async fn files(&self, options: RequestOptions) -> Result<ApiResponse> {
648 self.client
649 .request_json(Method::GET, "/find/file", options)
650 .await
651 }
652
653 pub async fn symbols(&self, options: RequestOptions) -> Result<ApiResponse> {
655 self.client
656 .request_json(Method::GET, "/find/symbol", options)
657 .await
658 }
659}
660
661#[derive(Debug, Clone)]
663pub struct SessionApi {
664 client: OpencodeClient,
665}
666
667impl SessionApi {
668 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
670 self.client
671 .request_json(Method::GET, "/session", options)
672 .await
673 }
674
675 pub async fn create(&self, options: RequestOptions) -> Result<ApiResponse> {
677 self.client
678 .request_json(Method::POST, "/session", options)
679 .await
680 }
681
682 pub async fn status(&self, options: RequestOptions) -> Result<ApiResponse> {
684 self.client
685 .request_json(Method::GET, "/session/status", options)
686 .await
687 }
688
689 pub async fn delete(&self, options: RequestOptions) -> Result<ApiResponse> {
691 self.client
692 .request_json(Method::DELETE, "/session/{sessionID}", options)
693 .await
694 }
695
696 pub async fn get(&self, options: RequestOptions) -> Result<ApiResponse> {
698 self.client
699 .request_json(Method::GET, "/session/{sessionID}", options)
700 .await
701 }
702
703 pub async fn update(&self, options: RequestOptions) -> Result<ApiResponse> {
705 self.client
706 .request_json(Method::PATCH, "/session/{sessionID}", options)
707 .await
708 }
709
710 pub async fn children(&self, options: RequestOptions) -> Result<ApiResponse> {
712 self.client
713 .request_json(Method::GET, "/session/{sessionID}/children", options)
714 .await
715 }
716
717 pub async fn todo(&self, options: RequestOptions) -> Result<ApiResponse> {
719 self.client
720 .request_json(Method::GET, "/session/{sessionID}/todo", options)
721 .await
722 }
723
724 pub async fn init(&self, options: RequestOptions) -> Result<ApiResponse> {
726 self.client
727 .request_json(Method::POST, "/session/{sessionID}/init", options)
728 .await
729 }
730
731 pub async fn fork(&self, options: RequestOptions) -> Result<ApiResponse> {
733 self.client
734 .request_json(Method::POST, "/session/{sessionID}/fork", options)
735 .await
736 }
737
738 pub async fn abort(&self, options: RequestOptions) -> Result<ApiResponse> {
740 self.client
741 .request_json(Method::POST, "/session/{sessionID}/abort", options)
742 .await
743 }
744
745 pub async fn share(&self, options: RequestOptions) -> Result<ApiResponse> {
747 self.client
748 .request_json(Method::POST, "/session/{sessionID}/share", options)
749 .await
750 }
751
752 pub async fn unshare(&self, options: RequestOptions) -> Result<ApiResponse> {
754 self.client
755 .request_json(Method::DELETE, "/session/{sessionID}/share", options)
756 .await
757 }
758
759 pub async fn diff(&self, options: RequestOptions) -> Result<ApiResponse> {
761 self.client
762 .request_json(Method::GET, "/session/{sessionID}/diff", options)
763 .await
764 }
765
766 pub async fn summarize(&self, options: RequestOptions) -> Result<ApiResponse> {
768 self.client
769 .request_json(Method::POST, "/session/{sessionID}/summarize", options)
770 .await
771 }
772
773 pub async fn messages(&self, options: RequestOptions) -> Result<ApiResponse> {
775 self.client
776 .request_json(Method::GET, "/session/{sessionID}/message", options)
777 .await
778 }
779
780 pub async fn prompt(&self, options: RequestOptions) -> Result<ApiResponse> {
782 self.client
783 .request_json(Method::POST, "/session/{sessionID}/message", options)
784 .await
785 }
786
787 pub async fn message(&self, options: RequestOptions) -> Result<ApiResponse> {
789 self.client
790 .request_json(
791 Method::GET,
792 "/session/{sessionID}/message/{messageID}",
793 options,
794 )
795 .await
796 }
797
798 pub async fn prompt_async(&self, options: RequestOptions) -> Result<ApiResponse> {
800 self.client
801 .request_json(Method::POST, "/session/{sessionID}/prompt_async", options)
802 .await
803 }
804
805 pub async fn command(&self, options: RequestOptions) -> Result<ApiResponse> {
807 self.client
808 .request_json(Method::POST, "/session/{sessionID}/command", options)
809 .await
810 }
811
812 pub async fn shell(&self, options: RequestOptions) -> Result<ApiResponse> {
814 self.client
815 .request_json(Method::POST, "/session/{sessionID}/shell", options)
816 .await
817 }
818
819 pub async fn revert(&self, options: RequestOptions) -> Result<ApiResponse> {
821 self.client
822 .request_json(Method::POST, "/session/{sessionID}/revert", options)
823 .await
824 }
825
826 pub async fn unrevert(&self, options: RequestOptions) -> Result<ApiResponse> {
828 self.client
829 .request_json(Method::POST, "/session/{sessionID}/unrevert", options)
830 .await
831 }
832
833 pub async fn delete_message(&self, options: RequestOptions) -> Result<ApiResponse> {
835 self.client
836 .request_json(
837 Method::DELETE,
838 "/session/{sessionID}/message/{messageID}",
839 options,
840 )
841 .await
842 }
843
844 pub async fn update_part(&self, options: RequestOptions) -> Result<ApiResponse> {
846 self.client
847 .request_json(
848 Method::PATCH,
849 "/session/{sessionID}/message/{messageID}/part/{partID}",
850 options,
851 )
852 .await
853 }
854
855 pub async fn delete_part(&self, options: RequestOptions) -> Result<ApiResponse> {
857 self.client
858 .request_json(
859 Method::DELETE,
860 "/session/{sessionID}/message/{messageID}/part/{partID}",
861 options,
862 )
863 .await
864 }
865
866 pub async fn respond_permission(&self, options: RequestOptions) -> Result<ApiResponse> {
868 self.client
869 .request_json(
870 Method::POST,
871 "/session/{sessionID}/permissions/{permissionID}",
872 options,
873 )
874 .await
875 }
876}
877
878#[derive(Debug, Clone)]
880pub struct GlobalApi {
881 client: OpencodeClient,
882}
883
884impl GlobalApi {
885 pub async fn health(&self, options: RequestOptions) -> Result<ApiResponse> {
886 self.client
887 .request_json(Method::GET, "/global/health", options)
888 .await
889 }
890
891 pub async fn dispose(&self, options: RequestOptions) -> Result<ApiResponse> {
892 self.client
893 .request_json(Method::POST, "/global/dispose", options)
894 .await
895 }
896
897 pub async fn config_get(&self, options: RequestOptions) -> Result<ApiResponse> {
898 self.client
899 .request_json(Method::GET, "/global/config", options)
900 .await
901 }
902
903 pub async fn config_update(&self, options: RequestOptions) -> Result<ApiResponse> {
904 self.client
905 .request_json(Method::PATCH, "/global/config", options)
906 .await
907 }
908
909 pub async fn event(&self, options: RequestOptions) -> Result<SseStream> {
910 self.client
911 .request_sse(Method::GET, "/global/event", options)
912 .await
913 }
914}
915
916#[derive(Debug, Clone)]
918pub struct AppApi {
919 client: OpencodeClient,
920}
921
922impl AppApi {
923 pub async fn agents(&self, options: RequestOptions) -> Result<ApiResponse> {
924 self.client.call_operation("app.agents", options).await
925 }
926
927 pub async fn log(&self, options: RequestOptions) -> Result<ApiResponse> {
928 self.client.call_operation("app.log", options).await
929 }
930
931 pub async fn skills(&self, options: RequestOptions) -> Result<ApiResponse> {
932 self.client.call_operation("app.skills", options).await
933 }
934}
935
936#[derive(Debug, Clone)]
938pub struct CommandApi {
939 client: OpencodeClient,
940}
941
942impl CommandApi {
943 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
944 self.client.call_operation("command.list", options).await
945 }
946}
947
948#[derive(Debug, Clone)]
950pub struct InstanceApi {
951 client: OpencodeClient,
952}
953
954impl InstanceApi {
955 pub async fn dispose(&self, options: RequestOptions) -> Result<ApiResponse> {
956 self.client
957 .call_operation("instance.dispose", options)
958 .await
959 }
960}
961
962#[derive(Debug, Clone)]
964pub struct ConfigApi {
965 client: OpencodeClient,
966}
967
968impl ConfigApi {
969 pub async fn get(&self, options: RequestOptions) -> Result<ApiResponse> {
970 self.client.call_operation("config.get", options).await
971 }
972
973 pub async fn update(&self, options: RequestOptions) -> Result<ApiResponse> {
974 self.client.call_operation("config.update", options).await
975 }
976
977 pub async fn providers(&self, options: RequestOptions) -> Result<ApiResponse> {
978 self.client
979 .call_operation("config.providers", options)
980 .await
981 }
982}
983
984#[derive(Debug, Clone)]
986pub struct ProjectApi {
987 client: OpencodeClient,
988}
989
990impl ProjectApi {
991 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
992 self.client
993 .request_json(Method::GET, "/project", options)
994 .await
995 }
996
997 pub async fn current(&self, options: RequestOptions) -> Result<ApiResponse> {
998 self.client
999 .request_json(Method::GET, "/project/current", options)
1000 .await
1001 }
1002
1003 pub async fn update(&self, options: RequestOptions) -> Result<ApiResponse> {
1004 self.client
1005 .request_json(Method::PATCH, "/project/{projectID}", options)
1006 .await
1007 }
1008}
1009
1010#[derive(Debug, Clone)]
1012pub struct PathApi {
1013 client: OpencodeClient,
1014}
1015
1016impl PathApi {
1017 pub async fn get(&self, options: RequestOptions) -> Result<ApiResponse> {
1018 self.client.call_operation("path.get", options).await
1019 }
1020}
1021
1022#[derive(Debug, Clone)]
1024pub struct FileApi {
1025 client: OpencodeClient,
1026}
1027
1028impl FileApi {
1029 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
1030 self.client
1031 .request_json(Method::GET, "/file", options)
1032 .await
1033 }
1034
1035 pub async fn read(&self, options: RequestOptions) -> Result<ApiResponse> {
1036 self.client
1037 .request_json(Method::GET, "/file/content", options)
1038 .await
1039 }
1040}
1041
1042#[derive(Debug, Clone)]
1044pub struct LspApi {
1045 client: OpencodeClient,
1046}
1047
1048impl LspApi {
1049 pub async fn status(&self, options: RequestOptions) -> Result<ApiResponse> {
1050 self.client.request_json(Method::GET, "/lsp", options).await
1051 }
1052}
1053
1054#[derive(Debug, Clone)]
1056pub struct ToolApi {
1057 client: OpencodeClient,
1058}
1059
1060impl ToolApi {
1061 pub async fn ids(&self, options: RequestOptions) -> Result<ApiResponse> {
1062 self.client
1063 .request_json(Method::GET, "/experimental/tool/ids", options)
1064 .await
1065 }
1066
1067 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
1068 self.client
1069 .request_json(Method::GET, "/experimental/tool", options)
1070 .await
1071 }
1072}
1073
1074#[derive(Debug, Clone)]
1076pub struct AuthApi {
1077 client: OpencodeClient,
1078}
1079
1080impl AuthApi {
1081 pub async fn set(&self, options: RequestOptions) -> Result<ApiResponse> {
1082 self.client.call_operation("auth.set", options).await
1083 }
1084
1085 pub async fn remove(&self, options: RequestOptions) -> Result<ApiResponse> {
1086 self.client.call_operation("auth.remove", options).await
1087 }
1088}
1089
1090#[derive(Debug, Clone)]
1092pub struct ProviderApi {
1093 client: OpencodeClient,
1094}
1095
1096impl ProviderApi {
1097 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
1098 self.client
1099 .request_json(Method::GET, "/provider", options)
1100 .await
1101 }
1102
1103 pub async fn auth(&self, options: RequestOptions) -> Result<ApiResponse> {
1104 self.client
1105 .request_json(Method::GET, "/provider/auth", options)
1106 .await
1107 }
1108
1109 pub fn oauth(&self) -> OauthApi {
1110 OauthApi {
1111 client: self.client.clone(),
1112 }
1113 }
1114}
1115
1116#[derive(Debug, Clone)]
1118pub struct OauthApi {
1119 client: OpencodeClient,
1120}
1121
1122impl OauthApi {
1123 pub async fn authorize(&self, options: RequestOptions) -> Result<ApiResponse> {
1124 self.client
1125 .request_json(Method::POST, "/provider/{id}/oauth/authorize", options)
1126 .await
1127 }
1128
1129 pub async fn callback(&self, options: RequestOptions) -> Result<ApiResponse> {
1130 self.client
1131 .request_json(Method::POST, "/provider/{id}/oauth/callback", options)
1132 .await
1133 }
1134}
1135
1136#[derive(Debug, Clone)]
1138pub struct McpApi {
1139 client: OpencodeClient,
1140}
1141
1142impl McpApi {
1143 pub async fn status(&self, options: RequestOptions) -> Result<ApiResponse> {
1144 self.client.request_json(Method::GET, "/mcp", options).await
1145 }
1146
1147 pub async fn add(&self, options: RequestOptions) -> Result<ApiResponse> {
1148 self.client
1149 .request_json(Method::POST, "/mcp", options)
1150 .await
1151 }
1152
1153 pub async fn connect(&self, options: RequestOptions) -> Result<ApiResponse> {
1154 self.client
1155 .request_json(Method::POST, "/mcp/{name}/connect", options)
1156 .await
1157 }
1158
1159 pub async fn disconnect(&self, options: RequestOptions) -> Result<ApiResponse> {
1160 self.client
1161 .request_json(Method::POST, "/mcp/{name}/disconnect", options)
1162 .await
1163 }
1164
1165 pub fn auth(&self) -> McpAuthApi {
1166 McpAuthApi {
1167 client: self.client.clone(),
1168 }
1169 }
1170}
1171
1172#[derive(Debug, Clone)]
1174pub struct McpAuthApi {
1175 client: OpencodeClient,
1176}
1177
1178impl McpAuthApi {
1179 pub async fn remove(&self, options: RequestOptions) -> Result<ApiResponse> {
1180 self.client
1181 .request_json(Method::DELETE, "/mcp/{name}/auth", options)
1182 .await
1183 }
1184
1185 pub async fn start(&self, options: RequestOptions) -> Result<ApiResponse> {
1186 self.client
1187 .request_json(Method::POST, "/mcp/{name}/auth", options)
1188 .await
1189 }
1190
1191 pub async fn callback(&self, options: RequestOptions) -> Result<ApiResponse> {
1192 self.client
1193 .request_json(Method::POST, "/mcp/{name}/auth/callback", options)
1194 .await
1195 }
1196
1197 pub async fn authenticate(&self, options: RequestOptions) -> Result<ApiResponse> {
1198 self.client
1199 .request_json(Method::POST, "/mcp/{name}/auth/authenticate", options)
1200 .await
1201 }
1202}
1203
1204#[derive(Debug, Clone)]
1206pub struct PtyApi {
1207 client: OpencodeClient,
1208}
1209
1210impl PtyApi {
1211 pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
1212 self.client.request_json(Method::GET, "/pty", options).await
1213 }
1214
1215 pub async fn create(&self, options: RequestOptions) -> Result<ApiResponse> {
1216 self.client
1217 .request_json(Method::POST, "/pty", options)
1218 .await
1219 }
1220
1221 pub async fn remove(&self, options: RequestOptions) -> Result<ApiResponse> {
1222 self.client
1223 .request_json(Method::DELETE, "/pty/{ptyID}", options)
1224 .await
1225 }
1226
1227 pub async fn get(&self, options: RequestOptions) -> Result<ApiResponse> {
1228 self.client
1229 .request_json(Method::GET, "/pty/{ptyID}", options)
1230 .await
1231 }
1232
1233 pub async fn update(&self, options: RequestOptions) -> Result<ApiResponse> {
1234 self.client
1235 .request_json(Method::PUT, "/pty/{ptyID}", options)
1236 .await
1237 }
1238
1239 pub async fn connect(&self, options: RequestOptions) -> Result<ApiResponse> {
1240 self.client
1241 .request_json(Method::GET, "/pty/{ptyID}/connect", options)
1242 .await
1243 }
1244}
1245
1246#[derive(Debug, Clone)]
1248pub struct EventApi {
1249 client: OpencodeClient,
1250}
1251
1252impl EventApi {
1253 pub async fn subscribe(&self, options: RequestOptions) -> Result<SseStream> {
1254 self.client
1255 .request_sse(Method::GET, "/event", options)
1256 .await
1257 }
1258}
1259
1260#[derive(Debug, Clone)]
1262pub struct FormatterApi {
1263 client: OpencodeClient,
1264}
1265
1266impl FormatterApi {
1267 pub async fn status(&self, options: RequestOptions) -> Result<ApiResponse> {
1268 self.client
1269 .call_operation("formatter.status", options)
1270 .await
1271 }
1272}
1273
1274#[derive(Debug, Clone)]
1276pub struct VcsApi {
1277 client: OpencodeClient,
1278}
1279
1280impl VcsApi {
1281 pub async fn get(&self, options: RequestOptions) -> Result<ApiResponse> {
1282 self.client.call_operation("vcs.get", options).await
1283 }
1284}
1285
1286#[derive(Debug, Clone)]
1288pub struct TuiApi {
1289 client: OpencodeClient,
1290}
1291
1292impl TuiApi {
1293 pub async fn append_prompt(&self, options: RequestOptions) -> Result<ApiResponse> {
1294 self.client
1295 .call_operation("tui.appendPrompt", options)
1296 .await
1297 }
1298
1299 pub async fn clear_prompt(&self, options: RequestOptions) -> Result<ApiResponse> {
1300 self.client.call_operation("tui.clearPrompt", options).await
1301 }
1302
1303 pub async fn execute_command(&self, options: RequestOptions) -> Result<ApiResponse> {
1304 self.client
1305 .call_operation("tui.executeCommand", options)
1306 .await
1307 }
1308
1309 pub async fn open_help(&self, options: RequestOptions) -> Result<ApiResponse> {
1310 self.client.call_operation("tui.openHelp", options).await
1311 }
1312
1313 pub async fn open_models(&self, options: RequestOptions) -> Result<ApiResponse> {
1314 self.client.call_operation("tui.openModels", options).await
1315 }
1316
1317 pub async fn open_sessions(&self, options: RequestOptions) -> Result<ApiResponse> {
1318 self.client
1319 .call_operation("tui.openSessions", options)
1320 .await
1321 }
1322
1323 pub async fn open_themes(&self, options: RequestOptions) -> Result<ApiResponse> {
1324 self.client.call_operation("tui.openThemes", options).await
1325 }
1326
1327 pub async fn publish(&self, options: RequestOptions) -> Result<ApiResponse> {
1328 self.client.call_operation("tui.publish", options).await
1329 }
1330
1331 pub async fn select_session(&self, options: RequestOptions) -> Result<ApiResponse> {
1332 self.client
1333 .call_operation("tui.selectSession", options)
1334 .await
1335 }
1336
1337 pub async fn show_toast(&self, options: RequestOptions) -> Result<ApiResponse> {
1338 self.client.call_operation("tui.showToast", options).await
1339 }
1340
1341 pub async fn submit_prompt(&self, options: RequestOptions) -> Result<ApiResponse> {
1342 self.client
1343 .call_operation("tui.submitPrompt", options)
1344 .await
1345 }
1346
1347 pub fn control(&self) -> TuiControlApi {
1348 TuiControlApi {
1349 client: self.client.clone(),
1350 }
1351 }
1352}
1353
1354#[derive(Debug, Clone)]
1356pub struct TuiControlApi {
1357 client: OpencodeClient,
1358}
1359
1360impl TuiControlApi {
1361 pub async fn next(&self, options: RequestOptions) -> Result<ApiResponse> {
1362 self.client
1363 .call_operation("tui.control.next", options)
1364 .await
1365 }
1366
1367 pub async fn response(&self, options: RequestOptions) -> Result<ApiResponse> {
1368 self.client
1369 .call_operation("tui.control.response", options)
1370 .await
1371 }
1372}
1373
1374pub type ControlApi = TuiControlApi;
1376
1377fn apply_sse_line(line: &str, current: &mut SseEvent) {
1378 if line.starts_with(':') {
1379 return;
1380 }
1381
1382 let (field, value) = match line.split_once(':') {
1383 Some((f, v)) => (f, v.trim_start()),
1384 None => (line, ""),
1385 };
1386
1387 match field {
1388 "event" => current.event = Some(value.to_string()),
1389 "id" => current.id = Some(value.to_string()),
1390 "retry" => {
1391 if let Ok(v) = value.parse::<u64>() {
1392 current.retry = Some(v);
1393 }
1394 }
1395 "data" => {
1396 if !current.data.is_empty() {
1397 current.data.push('\n');
1398 }
1399 current.data.push_str(value);
1400 }
1401 _ => {}
1402 }
1403}
1404
1405fn parse_success_body(bytes: &[u8]) -> Value {
1406 match serde_json::from_slice::<Value>(bytes) {
1407 Ok(json) => json,
1408 Err(_) => Value::String(String::from_utf8_lossy(bytes).to_string()),
1409 }
1410}
1411
1412fn headers_to_map(headers: &HeaderMap) -> HashMap<String, String> {
1413 headers
1414 .iter()
1415 .filter_map(|(k, v)| {
1416 v.to_str()
1417 .ok()
1418 .map(|value| (k.as_str().to_string(), value.to_string()))
1419 })
1420 .collect()
1421}
1422
1423fn encode_component(value: &str) -> String {
1424 utf8_percent_encode(value, COMPONENT_ENCODE_SET).to_string()
1425}
1426
1427fn encode_directory_header(value: &str) -> String {
1428 if value.is_ascii() {
1429 value.to_string()
1430 } else {
1431 encode_component(value)
1432 }
1433}
1434
1435fn resolve_path_value<'a>(
1436 params: &'a HashMap<String, String>,
1437 key: &str,
1438 allow_id_fallback: bool,
1439) -> Option<&'a str> {
1440 if let Some(v) = params.get(key) {
1441 return Some(v);
1442 }
1443
1444 if let Some(v) = params.get(&key.to_ascii_lowercase()) {
1445 return Some(v);
1446 }
1447
1448 if key.ends_with("ID") {
1449 let alt = key.trim_end_matches("ID");
1450 if let Some(v) = params.get(alt) {
1451 return Some(v);
1452 }
1453
1454 let snake = to_snake_case(alt);
1455 if let Some(v) = params.get(&snake) {
1456 return Some(v);
1457 }
1458
1459 let snake_id = format!("{}_id", snake);
1460 if let Some(v) = params.get(&snake_id) {
1461 return Some(v);
1462 }
1463 }
1464
1465 if allow_id_fallback {
1469 if let Some(v) = params.get("id") {
1470 return Some(v);
1471 }
1472 }
1473
1474 None
1475}
1476
1477fn to_snake_case(input: &str) -> String {
1478 let mut out = String::new();
1479 for (idx, ch) in input.chars().enumerate() {
1480 if ch.is_ascii_uppercase() {
1481 if idx > 0 {
1482 out.push('_');
1483 }
1484 out.push(ch.to_ascii_lowercase());
1485 } else {
1486 out.push(ch);
1487 }
1488 }
1489 out
1490}
1491
1492fn append_query_value(out: &mut Vec<(String, String)>, key: &str, value: &Value) {
1493 match value {
1494 Value::Null => {}
1495 Value::Bool(v) => {
1496 out.push((
1497 key.to_string(),
1498 if *v { "true" } else { "false" }.to_string(),
1499 ));
1500 }
1501 Value::Number(v) => {
1502 out.push((key.to_string(), v.to_string()));
1503 }
1504 Value::String(v) => {
1505 out.push((key.to_string(), v.clone()));
1506 }
1507 Value::Array(items) => {
1508 for item in items {
1509 append_query_value(out, key, item);
1510 }
1511 }
1512 Value::Object(_) => {
1513 out.push((key.to_string(), value.to_string()));
1514 }
1515 }
1516}
1517
1518fn operation_spec(operation_id: &str) -> Option<(Method, &'static str, bool)> {
1519 let (method, path, sse) = match operation_id {
1520 "app.agents" => ("GET", "/agent", false),
1521 "app.log" => ("POST", "/log", false),
1522 "app.skills" => ("GET", "/skill", false),
1523 "auth.remove" => ("DELETE", "/auth/{providerID}", false),
1524 "auth.set" => ("PUT", "/auth/{providerID}", false),
1525 "command.list" => ("GET", "/command", false),
1526 "config.get" => ("GET", "/config", false),
1527 "config.providers" => ("GET", "/config/providers", false),
1528 "config.update" => ("PATCH", "/config", false),
1529 "event.subscribe" => ("GET", "/event", true),
1530 "experimental.resource.list" => ("GET", "/experimental/resource", false),
1531 "experimental.session.list" => ("GET", "/experimental/session", false),
1532 "experimental.workspace.create" => ("POST", "/experimental/workspace", false),
1533 "experimental.workspace.list" => ("GET", "/experimental/workspace", false),
1534 "experimental.workspace.remove" => ("DELETE", "/experimental/workspace/{id}", false),
1535 "file.list" => ("GET", "/file", false),
1536 "file.read" => ("GET", "/file/content", false),
1537 "file.status" => ("GET", "/file/status", false),
1538 "find.files" => ("GET", "/find/file", false),
1539 "find.symbols" => ("GET", "/find/symbol", false),
1540 "find.text" => ("GET", "/find", false),
1541 "formatter.status" => ("GET", "/formatter", false),
1542 "global.config.get" => ("GET", "/global/config", false),
1543 "global.config.update" => ("PATCH", "/global/config", false),
1544 "global.dispose" => ("POST", "/global/dispose", false),
1545 "global.event" => ("GET", "/global/event", true),
1546 "global.health" => ("GET", "/global/health", false),
1547 "instance.dispose" => ("POST", "/instance/dispose", false),
1548 "lsp.status" => ("GET", "/lsp", false),
1549 "mcp.add" => ("POST", "/mcp", false),
1550 "mcp.auth.authenticate" => ("POST", "/mcp/{name}/auth/authenticate", false),
1551 "mcp.auth.callback" => ("POST", "/mcp/{name}/auth/callback", false),
1552 "mcp.auth.remove" => ("DELETE", "/mcp/{name}/auth", false),
1553 "mcp.auth.start" => ("POST", "/mcp/{name}/auth", false),
1554 "mcp.connect" => ("POST", "/mcp/{name}/connect", false),
1555 "mcp.disconnect" => ("POST", "/mcp/{name}/disconnect", false),
1556 "mcp.status" => ("GET", "/mcp", false),
1557 "part.delete" => (
1558 "DELETE",
1559 "/session/{sessionID}/message/{messageID}/part/{partID}",
1560 false,
1561 ),
1562 "part.update" => (
1563 "PATCH",
1564 "/session/{sessionID}/message/{messageID}/part/{partID}",
1565 false,
1566 ),
1567 "path.get" => ("GET", "/path", false),
1568 "permission.list" => ("GET", "/permission", false),
1569 "permission.reply" => ("POST", "/permission/{requestID}/reply", false),
1570 "permission.respond" => (
1571 "POST",
1572 "/session/{sessionID}/permissions/{permissionID}",
1573 false,
1574 ),
1575 "project.current" => ("GET", "/project/current", false),
1576 "project.list" => ("GET", "/project", false),
1577 "project.update" => ("PATCH", "/project/{projectID}", false),
1578 "provider.auth" => ("GET", "/provider/auth", false),
1579 "provider.list" => ("GET", "/provider", false),
1580 "provider.oauth.authorize" => ("POST", "/provider/{providerID}/oauth/authorize", false),
1581 "provider.oauth.callback" => ("POST", "/provider/{providerID}/oauth/callback", false),
1582 "pty.connect" => ("GET", "/pty/{ptyID}/connect", false),
1583 "pty.create" => ("POST", "/pty", false),
1584 "pty.get" => ("GET", "/pty/{ptyID}", false),
1585 "pty.list" => ("GET", "/pty", false),
1586 "pty.remove" => ("DELETE", "/pty/{ptyID}", false),
1587 "pty.update" => ("PUT", "/pty/{ptyID}", false),
1588 "question.list" => ("GET", "/question", false),
1589 "question.reject" => ("POST", "/question/{requestID}/reject", false),
1590 "question.reply" => ("POST", "/question/{requestID}/reply", false),
1591 "session.abort" => ("POST", "/session/{sessionID}/abort", false),
1592 "session.children" => ("GET", "/session/{sessionID}/children", false),
1593 "session.command" => ("POST", "/session/{sessionID}/command", false),
1594 "session.create" => ("POST", "/session", false),
1595 "session.delete" => ("DELETE", "/session/{sessionID}", false),
1596 "session.deleteMessage" => ("DELETE", "/session/{sessionID}/message/{messageID}", false),
1597 "session.diff" => ("GET", "/session/{sessionID}/diff", false),
1598 "session.fork" => ("POST", "/session/{sessionID}/fork", false),
1599 "session.get" => ("GET", "/session/{sessionID}", false),
1600 "session.init" => ("POST", "/session/{sessionID}/init", false),
1601 "session.list" => ("GET", "/session", false),
1602 "session.message" => ("GET", "/session/{sessionID}/message/{messageID}", false),
1603 "session.messages" => ("GET", "/session/{sessionID}/message", false),
1604 "session.prompt" => ("POST", "/session/{sessionID}/message", false),
1605 "session.prompt_async" => ("POST", "/session/{sessionID}/prompt_async", false),
1606 "session.revert" => ("POST", "/session/{sessionID}/revert", false),
1607 "session.share" => ("POST", "/session/{sessionID}/share", false),
1608 "session.shell" => ("POST", "/session/{sessionID}/shell", false),
1609 "session.status" => ("GET", "/session/status", false),
1610 "session.summarize" => ("POST", "/session/{sessionID}/summarize", false),
1611 "session.todo" => ("GET", "/session/{sessionID}/todo", false),
1612 "session.unrevert" => ("POST", "/session/{sessionID}/unrevert", false),
1613 "session.unshare" => ("DELETE", "/session/{sessionID}/share", false),
1614 "session.update" => ("PATCH", "/session/{sessionID}", false),
1615 "tool.ids" => ("GET", "/experimental/tool/ids", false),
1616 "tool.list" => ("GET", "/experimental/tool", false),
1617 "tui.appendPrompt" => ("POST", "/tui/append-prompt", false),
1618 "tui.clearPrompt" => ("POST", "/tui/clear-prompt", false),
1619 "tui.control.next" => ("GET", "/tui/control/next", false),
1620 "tui.control.response" => ("POST", "/tui/control/response", false),
1621 "tui.executeCommand" => ("POST", "/tui/execute-command", false),
1622 "tui.openHelp" => ("POST", "/tui/open-help", false),
1623 "tui.openModels" => ("POST", "/tui/open-models", false),
1624 "tui.openSessions" => ("POST", "/tui/open-sessions", false),
1625 "tui.openThemes" => ("POST", "/tui/open-themes", false),
1626 "tui.publish" => ("POST", "/tui/publish", false),
1627 "tui.selectSession" => ("POST", "/tui/select-session", false),
1628 "tui.showToast" => ("POST", "/tui/show-toast", false),
1629 "tui.submitPrompt" => ("POST", "/tui/submit-prompt", false),
1630 "vcs.get" => ("GET", "/vcs", false),
1631 "worktree.create" => ("POST", "/experimental/worktree", false),
1632 "worktree.list" => ("GET", "/experimental/worktree", false),
1633 "worktree.remove" => ("DELETE", "/experimental/worktree", false),
1634 "worktree.reset" => ("POST", "/experimental/worktree/reset", false),
1635 _ => return None,
1636 };
1637
1638 Some((Method::from_bytes(method.as_bytes()).ok()?, path, sse))
1639}