#[cfg(not(feature = "http"))]
use crate::error::OpencodeError;
use crate::error::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[cfg(feature = "http")]
use crate::http::HttpClient;
#[cfg(feature = "http")]
use crate::http::HttpConfig;
#[derive(Clone)]
pub struct Client {
#[cfg(feature = "http")]
http: HttpClient,
last_event_id: Arc<RwLock<Option<String>>>,
}
#[derive(Clone)]
pub struct ClientBuilder {
base_url: String,
directory: Option<String>,
timeout: Duration,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self {
base_url: "http://127.0.0.1:4096".to_string(),
directory: None,
timeout: Duration::from_secs(1800), }
}
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
#[must_use]
pub fn directory(mut self, dir: impl Into<String>) -> Self {
self.directory = Some(dir.into());
self
}
#[must_use]
pub fn timeout_secs(mut self, secs: u64) -> Self {
self.timeout = Duration::from_secs(secs);
self
}
#[cfg(feature = "http")]
pub fn build(self) -> Result<Client> {
let http = HttpClient::new(HttpConfig {
base_url: self.base_url,
directory: self.directory,
timeout: self.timeout,
})?;
Ok(Client {
http,
last_event_id: Arc::new(RwLock::new(None)),
})
}
#[cfg(not(feature = "http"))]
pub fn build(self) -> Result<Client> {
Err(OpencodeError::InvalidConfig(
"http feature required to build client".into(),
))
}
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
#[cfg(feature = "http")]
pub fn sessions(&self) -> crate::http::sessions::SessionsApi {
crate::http::sessions::SessionsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn messages(&self) -> crate::http::messages::MessagesApi {
crate::http::messages::MessagesApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn parts(&self) -> crate::http::parts::PartsApi {
crate::http::parts::PartsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn permissions(&self) -> crate::http::permissions::PermissionsApi {
crate::http::permissions::PermissionsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn files(&self) -> crate::http::files::FilesApi {
crate::http::files::FilesApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn find(&self) -> crate::http::find::FindApi {
crate::http::find::FindApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn providers(&self) -> crate::http::providers::ProvidersApi {
crate::http::providers::ProvidersApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn mcp(&self) -> crate::http::mcp::McpApi {
crate::http::mcp::McpApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn pty(&self) -> crate::http::pty::PtyApi {
crate::http::pty::PtyApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn config(&self) -> crate::http::config::ConfigApi {
crate::http::config::ConfigApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn tools(&self) -> crate::http::tools::ToolsApi {
crate::http::tools::ToolsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn project(&self) -> crate::http::project::ProjectApi {
crate::http::project::ProjectApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn worktree(&self) -> crate::http::worktree::WorktreeApi {
crate::http::worktree::WorktreeApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn misc(&self) -> crate::http::misc::MiscApi {
crate::http::misc::MiscApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn question(&self) -> crate::http::question::QuestionApi {
crate::http::question::QuestionApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn skills(&self) -> crate::http::skills::SkillsApi {
crate::http::skills::SkillsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn snapshots(&self) -> crate::http::snapshots::SnapshotsApi {
crate::http::snapshots::SnapshotsApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn resource(&self) -> crate::http::resource::ResourceApi {
crate::http::resource::ResourceApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub fn global(&self) -> crate::http::global::GlobalApi {
crate::http::global::GlobalApi::new(self.http.clone())
}
#[cfg(feature = "http")]
pub async fn run_simple_text(
&self,
text: impl Into<String>,
) -> Result<crate::types::session::Session> {
use crate::types::message::PromptPart;
use crate::types::message::PromptRequest;
use crate::types::session::CreateSessionRequest;
let session = self
.sessions()
.create(&CreateSessionRequest::default())
.await?;
let _ = self
.messages()
.prompt(
&session.id,
&PromptRequest {
parts: vec![PromptPart::Text {
text: text.into(),
synthetic: None,
ignored: None,
metadata: None,
}],
message_id: None,
model: None,
agent: None,
no_reply: None,
system: None,
variant: None,
},
)
.await?;
Ok(session)
}
#[cfg(feature = "sse")]
#[expect(dead_code)] pub(crate) async fn set_last_event_id(&self, id: Option<String>) {
*self.last_event_id.write().await = id;
}
#[cfg(feature = "sse")]
#[expect(dead_code)] pub(crate) async fn last_event_id(&self) -> Option<String> {
self.last_event_id.read().await.clone()
}
#[cfg(feature = "http")]
#[expect(dead_code)] pub(crate) fn http(&self) -> &HttpClient {
&self.http
}
#[cfg(feature = "sse")]
#[expect(dead_code)] pub(crate) fn last_event_id_handle(&self) -> Arc<RwLock<Option<String>>> {
Arc::clone(&self.last_event_id)
}
}
#[cfg(all(feature = "http", feature = "sse"))]
impl Client {
pub fn sse_subscriber(&self) -> crate::sse::SseSubscriber {
crate::sse::SseSubscriber::new(
self.http.base().to_string(),
self.http.directory().map(std::string::ToString::to_string),
Arc::clone(&self.last_event_id),
)
}
pub fn subscribe(&self) -> Result<crate::sse::SseSubscription> {
self.sse_subscriber()
.subscribe(crate::sse::SseOptions::default())
}
pub fn subscribe_session(&self, session_id: &str) -> Result<crate::sse::SseSubscription> {
self.sse_subscriber()
.subscribe_session(session_id, crate::sse::SseOptions::default())
}
pub fn subscribe_global(&self) -> Result<crate::sse::SseSubscription> {
self.sse_subscriber()
.subscribe_global(crate::sse::SseOptions::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_builder_defaults() {
let builder = ClientBuilder::new();
assert_eq!(builder.base_url, "http://127.0.0.1:4096");
assert_eq!(builder.timeout, Duration::from_secs(1800));
assert!(builder.directory.is_none());
}
#[test]
fn test_client_builder_customization() {
let builder = ClientBuilder::new()
.base_url("http://localhost:8080")
.directory("/my/project")
.timeout_secs(60);
assert_eq!(builder.base_url, "http://localhost:8080");
assert_eq!(builder.directory, Some("/my/project".to_string()));
assert_eq!(builder.timeout, Duration::from_secs(60));
}
#[cfg(feature = "http")]
#[test]
fn test_client_build() {
let client = ClientBuilder::new().build();
assert!(client.is_ok());
}
}