Skip to main content

opencode/
client.rs

1//! OpenCode HTTP client and endpoint namespaces.
2//!
3//! This module provides:
4//! - low-level request primitives (`request_json`, `request_sse`)
5//! - namespace wrappers for OpenCode endpoints
6//! - operation-id dispatch helpers aligned with OpenCode OpenAPI specs
7
8use 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
23// Approximation of JS encodeURIComponent behavior for header/path usage.
24const 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/// HTTP request options compatible with OpenCode API endpoints.
51#[derive(Debug, Clone, Default)]
52pub struct RequestOptions {
53    /// Path parameters used to replace placeholders in endpoint template.
54    pub path: HashMap<String, String>,
55    /// Query string parameters.
56    pub query: HashMap<String, Value>,
57    /// Request headers.
58    pub headers: HashMap<String, String>,
59    /// JSON body payload.
60    pub body: Option<Value>,
61}
62
63impl RequestOptions {
64    /// Adds or overrides one path parameter.
65    ///
66    /// These values are used to render placeholders such as `{sessionID}`.
67    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    /// Adds or overrides one query parameter.
73    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    /// Adds or overrides one request header.
79    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    /// Sets a JSON request body.
85    pub fn with_body(mut self, value: Value) -> Self {
86        self.body = Some(value);
87        self
88    }
89}
90
91/// Unified JSON response envelope from OpenCode API calls.
92#[derive(Debug, Clone)]
93pub struct ApiResponse {
94    /// Parsed JSON payload. For 204 responses this is `{}`.
95    pub data: Value,
96    /// HTTP status code.
97    pub status: u16,
98    /// Response headers.
99    pub headers: HashMap<String, String>,
100}
101
102/// Parsed SSE event from OpenCode streaming endpoints.
103#[derive(Debug, Clone, Default, PartialEq, Eq)]
104pub struct SseEvent {
105    /// Event type (`event:` line).
106    pub event: Option<String>,
107    /// Event id (`id:` line).
108    pub id: Option<String>,
109    /// Retry hint (`retry:` line).
110    pub retry: Option<u64>,
111    /// Event data payload (joined `data:` lines).
112    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
121/// Type alias for async SSE stream.
122pub type SseStream = Pin<Box<dyn Stream<Item = Result<SseEvent>> + Send>>;
123
124/// Config for creating OpenCode HTTP client.
125#[derive(Clone)]
126pub struct OpencodeClientConfig {
127    /// Base URL for API requests. Defaults to `http://127.0.0.1:4096`.
128    pub base_url: String,
129    /// Optional project directory mapped to `x-opencode-directory` header.
130    pub directory: Option<String>,
131    /// Optional default headers.
132    pub headers: HashMap<String, String>,
133    /// Optional bearer token added as `Authorization: Bearer ...`.
134    pub bearer_token: Option<String>,
135    /// Request timeout.
136    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/// OpenCode API client aligned with official JS SDK request semantics.
182#[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
195/// Creates an OpenCode HTTP client from config.
196///
197/// The client applies:
198/// - default headers from `config.headers`
199/// - `Authorization: Bearer ...` when `bearer_token` is set
200/// - `x-opencode-directory` when `directory` is set
201/// - a request timeout from `config.timeout`
202pub 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    /// Returns session endpoint APIs (`/session/...`).
243    pub fn session(&self) -> SessionApi {
244        SessionApi {
245            client: self.clone(),
246        }
247    }
248
249    /// Returns app-level endpoint APIs.
250    pub fn app(&self) -> AppApi {
251        AppApi {
252            client: self.clone(),
253        }
254    }
255
256    /// Returns global endpoint APIs.
257    pub fn global(&self) -> GlobalApi {
258        GlobalApi {
259            client: self.clone(),
260        }
261    }
262
263    /// Returns command endpoint APIs.
264    pub fn command(&self) -> CommandApi {
265        CommandApi {
266            client: self.clone(),
267        }
268    }
269
270    /// Returns config endpoint APIs.
271    pub fn config(&self) -> ConfigApi {
272        ConfigApi {
273            client: self.clone(),
274        }
275    }
276
277    /// Returns project endpoint APIs.
278    pub fn project(&self) -> ProjectApi {
279        ProjectApi {
280            client: self.clone(),
281        }
282    }
283
284    /// Returns path endpoint APIs.
285    pub fn path(&self) -> PathApi {
286        PathApi {
287            client: self.clone(),
288        }
289    }
290
291    /// Returns file endpoint APIs.
292    pub fn file(&self) -> FileApi {
293        FileApi {
294            client: self.clone(),
295        }
296    }
297
298    /// Returns LSP endpoint APIs.
299    pub fn lsp(&self) -> LspApi {
300        LspApi {
301            client: self.clone(),
302        }
303    }
304
305    /// Returns tool endpoint APIs.
306    pub fn tool(&self) -> ToolApi {
307        ToolApi {
308            client: self.clone(),
309        }
310    }
311
312    /// Returns provider endpoint APIs.
313    pub fn provider(&self) -> ProviderApi {
314        ProviderApi {
315            client: self.clone(),
316        }
317    }
318
319    /// Returns auth endpoint APIs.
320    pub fn auth(&self) -> AuthApi {
321        AuthApi {
322            client: self.clone(),
323        }
324    }
325
326    /// Returns MCP endpoint APIs.
327    pub fn mcp(&self) -> McpApi {
328        McpApi {
329            client: self.clone(),
330        }
331    }
332
333    /// Returns PTY endpoint APIs.
334    pub fn pty(&self) -> PtyApi {
335        PtyApi {
336            client: self.clone(),
337        }
338    }
339
340    /// Returns event endpoint APIs.
341    pub fn event(&self) -> EventApi {
342        EventApi {
343            client: self.clone(),
344        }
345    }
346
347    /// Returns formatter endpoint APIs.
348    pub fn formatter(&self) -> FormatterApi {
349        FormatterApi {
350            client: self.clone(),
351        }
352    }
353
354    /// Returns find endpoint APIs.
355    pub fn find(&self) -> FindApi {
356        FindApi {
357            client: self.clone(),
358        }
359    }
360
361    /// Returns instance endpoint APIs.
362    pub fn instance(&self) -> InstanceApi {
363        InstanceApi {
364            client: self.clone(),
365        }
366    }
367
368    /// Returns VCS endpoint APIs.
369    pub fn vcs(&self) -> VcsApi {
370        VcsApi {
371            client: self.clone(),
372        }
373    }
374
375    /// Returns TUI endpoint APIs.
376    pub fn tui(&self) -> TuiApi {
377        TuiApi {
378            client: self.clone(),
379        }
380    }
381
382    /// Backward-compatible shorthand for TUI control endpoints.
383    pub fn control(&self) -> ControlApi {
384        ControlApi {
385            client: self.clone(),
386        }
387    }
388
389    /// Execute any OpenCode operation by official `operationId`.
390    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    /// Execute SSE operation by official `operationId` (`global.event` / `event.subscribe`).
411    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    /// Sends one HTTP request and returns a parsed JSON (or text) response envelope.
432    ///
433    /// For `2xx` responses:
434    /// - `204` or empty body -> `{}` payload
435    /// - valid JSON body -> parsed JSON
436    /// - non-JSON body -> string payload
437    ///
438    /// For non-`2xx` responses, returns [`Error::Api`] with status and raw body.
439    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    /// Sends one HTTP request and parses the response as Server-Sent Events.
472    ///
473    /// The parser supports:
474    /// - split UTF-8 across chunks
475    /// - multi-line `data:` fields
476    /// - trailing final lines without a terminating blank line
477    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/// Find endpoint namespace.
633#[derive(Debug, Clone)]
634pub struct FindApi {
635    client: OpencodeClient,
636}
637
638impl FindApi {
639    /// Searches text content.
640    pub async fn text(&self, options: RequestOptions) -> Result<ApiResponse> {
641        self.client
642            .request_json(Method::GET, "/find", options)
643            .await
644    }
645
646    /// Searches files by query/pattern.
647    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    /// Searches symbols.
654    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/// Session endpoint namespace.
662#[derive(Debug, Clone)]
663pub struct SessionApi {
664    client: OpencodeClient,
665}
666
667impl SessionApi {
668    /// Lists sessions.
669    pub async fn list(&self, options: RequestOptions) -> Result<ApiResponse> {
670        self.client
671            .request_json(Method::GET, "/session", options)
672            .await
673    }
674
675    /// Creates a new session.
676    pub async fn create(&self, options: RequestOptions) -> Result<ApiResponse> {
677        self.client
678            .request_json(Method::POST, "/session", options)
679            .await
680    }
681
682    /// Returns session runtime status.
683    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    /// Deletes a session.
690    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    /// Gets one session by id.
697    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    /// Updates mutable session fields.
704    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    /// Lists children for a session.
711    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    /// Returns session todo items.
718    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    /// Initializes a session.
725    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    /// Forks a session.
732    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    /// Aborts the active run in a session.
739    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    /// Shares a session.
746    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    /// Revokes sharing for a session.
753    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    /// Gets session diff.
760    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    /// Triggers session summarization.
767    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    /// Lists messages in a session.
774    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    /// Sends a prompt message to a session.
781    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    /// Gets one message by id.
788    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    /// Enqueues an async prompt run.
799    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    /// Sends a command to a session.
806    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    /// Executes a shell action in a session.
813    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    /// Reverts one message in session history.
820    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    /// Restores all reverted messages.
827    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    /// Deletes one message.
834    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    /// Updates one message part.
845    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    /// Deletes one message part.
856    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    /// Responds to a permission request under one session.
867    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/// Global endpoint namespace.
879#[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/// App endpoint namespace.
917#[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/// Command endpoint namespace.
937#[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/// Instance endpoint namespace.
949#[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/// Config endpoint namespace.
963#[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/// Project endpoint namespace.
985#[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/// Path endpoint namespace.
1011#[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/// File endpoint namespace.
1023#[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/// LSP endpoint namespace.
1043#[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/// Tool endpoint namespace.
1055#[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/// Auth endpoint namespace.
1075#[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/// Provider endpoint namespace.
1091#[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/// OAuth endpoint namespace under provider routes.
1117#[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/// MCP endpoint namespace.
1137#[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/// MCP auth endpoint namespace.
1173#[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/// PTY endpoint namespace.
1205#[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/// Event endpoint namespace.
1247#[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/// Formatter endpoint namespace.
1261#[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/// VCS endpoint namespace.
1275#[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/// TUI endpoint namespace.
1287#[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/// TUI control endpoint namespace.
1355#[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
1374/// Backward-compatible alias for top-level control API access.
1375pub 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    // Compatibility with official JS SDK v1 shape that frequently uses {id}
1466    // for single-parameter routes. Avoid this fallback on multi-parameter
1467    // routes (e.g. {sessionID}/{messageID}) to prevent accidental substitution.
1468    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}