Skip to main content

clawdb_client/
client.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::{Client as HttpClient, header};
5use serde_json::{json, Value};
6use tracing::info;
7use uuid::Uuid;
8
9use crate::builder::ClawDBBuilder;
10use crate::error::{SdkError, SdkResult};
11use crate::models::{BranchInfo, DiffResult, MergeResult, SearchHit, SearchOptions, SyncResult};
12
13#[derive(Clone)]
14pub struct ClawDBClient {
15    inner: ClawDB,
16}
17
18#[derive(Clone)]
19pub struct ClawDB {
20    inner: Arc<ClawDBInner>,
21}
22
23struct ClawDBInner {
24    endpoint: String,
25    api_key: String,
26    agent_id: String,
27    workspace: String,
28    role: String,
29    timeout_ms: u64,
30    http: HttpClient,
31    token: tokio::sync::RwLock<Option<String>>,
32}
33
34impl ClawDB {
35    /// Create a builder to configure and construct a ClawDB client.
36    pub fn builder() -> ClawDBBuilder {
37        ClawDBBuilder::new()
38    }
39
40    /// Create a client from environment variables.
41    pub async fn from_env() -> SdkResult<Self> {
42        ClawDBBuilder::from_env().build().await
43    }
44
45    /// Automatically provision a usable endpoint following the SDK fallback order.
46    pub async fn auto_provision() -> SdkResult<Self> {
47        if let Ok(endpoint) = std::env::var("CLAWDB_URL") {
48            return ClawDBBuilder::from_env().endpoint(endpoint).build().await;
49        }
50
51        if let Ok(api_key) = std::env::var("CLAWDB_API_KEY") {
52            return ClawDBBuilder::from_env()
53                .api_key(api_key)
54                .endpoint("https://cloud.clawdb.dev")
55                .build()
56                .await;
57        }
58
59        if tokio::net::TcpStream::connect("127.0.0.1:50050").await.is_ok() {
60            return ClawDBBuilder::from_env()
61                .endpoint("http://localhost:50050")
62                .build()
63                .await;
64        }
65
66        let candidates = [
67            "clawdb-server".to_string(),
68            dirs::home_dir()
69                .unwrap_or_default()
70                .join(".clawdb/bin/clawdb-server")
71                .display()
72                .to_string(),
73        ];
74
75        for candidate in candidates {
76            if std::process::Command::new(&candidate)
77                .arg("--port")
78                .arg("50050")
79                .spawn()
80                .is_ok()
81            {
82                tokio::time::sleep(Duration::from_millis(250)).await;
83                return ClawDBBuilder::from_env()
84                    .endpoint("http://localhost:50050")
85                    .build()
86                    .await;
87            }
88        }
89
90        Err(SdkError::Config("could not auto-provision clawdb-server".into()))
91    }
92
93    /// Create a client from an API key and endpoint.
94    pub async fn from_api_key(api_key: impl Into<String>, endpoint: impl Into<String>) -> SdkResult<Self> {
95        ClawDBBuilder::new()
96            .api_key(api_key)
97            .endpoint(endpoint)
98            .build()
99            .await
100    }
101
102    pub(crate) async fn new_internal(
103        endpoint: String,
104        api_key: String,
105        agent_id: String,
106        workspace: String,
107        role: String,
108        timeout_ms: u64,
109    ) -> SdkResult<Self> {
110        let mut default_headers = header::HeaderMap::new();
111        if !api_key.is_empty() {
112            default_headers.insert(
113                "X-Api-Key",
114                header::HeaderValue::from_str(&api_key)
115                    .map_err(|_| SdkError::Config("Invalid API key characters".into()))?,
116            );
117        }
118
119        let http = HttpClient::builder()
120            .default_headers(default_headers)
121            .timeout(std::time::Duration::from_millis(timeout_ms))
122            .build()
123            .map_err(|e| SdkError::Config(e.to_string()))?;
124
125        Ok(Self {
126            inner: Arc::new(ClawDBInner {
127                endpoint,
128                api_key,
129                agent_id,
130                workspace,
131                role,
132                timeout_ms,
133                http,
134                token: tokio::sync::RwLock::new(None),
135            }),
136        })
137    }
138
139    async fn auth_headers(&self) -> Vec<(String, String)> {
140        let token = self.inner.token.read().await;
141        if let Some(t) = token.as_deref() {
142            vec![("Authorization".into(), format!("Bearer {t}"))]
143        } else if !self.inner.api_key.is_empty() {
144            vec![("X-Api-Key".into(), self.inner.api_key.clone())]
145        } else {
146            vec![]
147        }
148    }
149
150    async fn post(&self, path: &str, body: Value) -> SdkResult<Value> {
151        let url = format!("{}{}", self.inner.endpoint, path);
152        let mut req = self.inner.http.post(&url).json(&body);
153        for (k, v) in self.auth_headers().await {
154            req = req.header(k, v);
155        }
156        let resp = req.send().await.map_err(|e| SdkError::Reqwest(e))?;
157        let status = resp.status().as_u16();
158        let text = resp.text().await.map_err(|e| SdkError::Reqwest(e))?;
159        if status >= 400 {
160            return Err(SdkError::from_http(status, &text));
161        }
162        serde_json::from_str(&text).map_err(SdkError::Serialization)
163    }
164
165    /// Store a memory in ClawDB.
166    pub async fn remember(&self, content: impl Into<String>) -> SdkResult<Uuid> {
167        let content = content.into();
168        if content.trim().is_empty() {
169            return Err(SdkError::Validation {
170                field: "content".into(),
171                constraint: "must be non-empty".into(),
172            });
173        }
174        let resp = self.post("/v1/memory/remember", json!({"content": content, "agent_id": self.inner.agent_id})).await?;
175        let id_str = resp["memory_id"].as_str().unwrap_or_default();
176        Uuid::parse_str(id_str).map_err(|_| SdkError::Internal("Invalid UUID in response".into()))
177    }
178
179    /// Search memories semantically.
180    pub async fn search(&self, query: impl Into<String>, opts: SearchOptions) -> SdkResult<Vec<SearchHit>> {
181        let query = query.into();
182        let top_k = opts.top_k.unwrap_or(5);
183        if top_k > 100 {
184            return Err(SdkError::Validation { field: "top_k".into(), constraint: "must be <= 100".into() });
185        }
186        let resp = self.post("/v1/memory/search", json!({"query": query, "top_k": top_k, "semantic": opts.semantic.unwrap_or(true), "alpha": opts.alpha.unwrap_or(0.7)})).await?;
187        let results: Vec<SearchHit> = serde_json::from_value(resp["results"].clone())?;
188        Ok(results)
189    }
190
191    /// Search and return only the top result content.
192    pub async fn search_top_k(&self, query: impl Into<String>, k: u32) -> SdkResult<Vec<String>> {
193        let results = self.search(query, SearchOptions { top_k: Some(k), ..Default::default() }).await?;
194        Ok(results.into_iter().map(|r| r.content).collect())
195    }
196
197    /// Fork a new memory branch.
198    pub async fn fork(&self, name: impl Into<String>, parent: Option<&str>) -> SdkResult<BranchInfo> {
199        let resp = self.post("/v1/branches/fork", json!({"name": name.into(), "parent": parent.unwrap_or("trunk")})).await?;
200        Ok(serde_json::from_value(resp["branch"].clone())?)
201    }
202
203    /// Merge a branch into a target.
204    pub async fn merge(&self, source: impl Into<String>, into: &str, strategy: &str) -> SdkResult<MergeResult> {
205        let resp = self.post("/v1/branches/merge", json!({"source": source.into(), "into": into, "strategy": strategy})).await?;
206        Ok(serde_json::from_value(resp)?)
207    }
208
209    /// Diff two branches.
210    pub async fn diff(&self, branch_a: &str, branch_b: &str) -> SdkResult<DiffResult> {
211        let resp = self.post("/v1/branches/diff", json!({"branch_a": branch_a, "branch_b": branch_b})).await?;
212        Ok(serde_json::from_value(resp)?)
213    }
214
215    /// Push memories to ClawDB Cloud.
216    pub async fn sync_push(&self) -> SdkResult<SyncResult> {
217        let resp = self.post("/v1/sync/push", json!({})).await?;
218        Ok(serde_json::from_value(resp)?)
219    }
220
221    /// Trigger a reflection job.
222    pub async fn reflect(&self, job_type: &str) -> SdkResult<String> {
223        let resp = self.post("/v1/reflect/trigger", json!({"job_type": job_type})).await?;
224        Ok(resp["job_id"].as_str().unwrap_or_default().to_string())
225    }
226
227    /// Close the client (no-op for HTTP; placeholder for gRPC future).
228    pub async fn close(&self) {
229        info!("clawdb.close");
230    }
231}
232
233impl ClawDBClient {
234    /// Auto-provision a client endpoint.
235    pub async fn auto_provision() -> SdkResult<Self> {
236        Ok(Self {
237            inner: ClawDB::auto_provision().await?,
238        })
239    }
240
241    /// Create a builder for constructing a client.
242    pub fn builder() -> ClientBuilder {
243        ClientBuilder { inner: ClawDBBuilder::new() }
244    }
245
246    /// Access the memory namespace.
247    pub fn memory(&self) -> MemoryNamespace {
248        MemoryNamespace { client: self.inner.clone() }
249    }
250
251    /// Close the client.
252    pub async fn close(&self) {
253        self.inner.close().await;
254    }
255}
256
257/// Builder for [`ClawDBClient`].
258pub struct ClientBuilder {
259    inner: ClawDBBuilder,
260}
261
262impl ClientBuilder {
263    /// Set the endpoint.
264    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
265        self.inner = self.inner.endpoint(endpoint);
266        self
267    }
268
269    /// Set the agent id.
270    pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
271        self.inner = self.inner.agent_id(agent_id);
272        self
273    }
274
275    /// Build the client.
276    pub async fn build(self) -> SdkResult<ClawDBClient> {
277        Ok(ClawDBClient { inner: self.inner.build().await? })
278    }
279}
280
281/// Memory namespace entrypoint.
282pub struct MemoryNamespace {
283    client: ClawDB,
284}
285
286impl MemoryNamespace {
287    /// Remember content using a fluent builder.
288    pub fn remember(&self, content: impl Into<String>) -> RememberCall {
289        RememberCall { client: self.client.clone(), content: content.into() }
290    }
291
292    /// Search memories using a fluent builder.
293    pub fn search(&self, query: impl Into<String>) -> SearchCall {
294        SearchCall { client: self.client.clone(), query: query.into(), opts: SearchOptions::default() }
295    }
296}
297
298/// Fluent remember call builder.
299pub struct RememberCall {
300    client: ClawDB,
301    content: String,
302}
303
304impl RememberCall {
305    /// Execute the remember call.
306    pub async fn await_result(self) -> SdkResult<Uuid> {
307        self.client.remember(self.content).await
308    }
309}
310
311impl std::future::IntoFuture for RememberCall {
312    type Output = SdkResult<Uuid>;
313    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
314
315    fn into_future(self) -> Self::IntoFuture {
316        Box::pin(async move { self.client.remember(self.content).await })
317    }
318}
319
320/// Fluent memory search builder.
321pub struct SearchCall {
322    client: ClawDB,
323    query: String,
324    opts: SearchOptions,
325}
326
327impl SearchCall {
328    /// Set the maximum number of hits.
329    pub fn top_k(mut self, top_k: u32) -> Self {
330        self.opts.top_k = Some(top_k);
331        self
332    }
333
334    /// Execute the search call.
335    pub async fn call(self) -> SdkResult<Vec<SearchHit>> {
336        self.client.search(self.query, self.opts).await
337    }
338}
339
340impl std::fmt::Debug for ClawDB {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        f.debug_struct("ClawDB")
343            .field("endpoint", &self.inner.endpoint)
344            .field("agent_id", &self.inner.agent_id)
345            .finish()
346    }
347}