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 pub fn builder() -> ClawDBBuilder {
37 ClawDBBuilder::new()
38 }
39
40 pub async fn from_env() -> SdkResult<Self> {
42 ClawDBBuilder::from_env().build().await
43 }
44
45 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 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 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 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 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 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 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 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 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 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 pub async fn close(&self) {
229 info!("clawdb.close");
230 }
231}
232
233impl ClawDBClient {
234 pub async fn auto_provision() -> SdkResult<Self> {
236 Ok(Self {
237 inner: ClawDB::auto_provision().await?,
238 })
239 }
240
241 pub fn builder() -> ClientBuilder {
243 ClientBuilder { inner: ClawDBBuilder::new() }
244 }
245
246 pub fn memory(&self) -> MemoryNamespace {
248 MemoryNamespace { client: self.inner.clone() }
249 }
250
251 pub async fn close(&self) {
253 self.inner.close().await;
254 }
255}
256
257pub struct ClientBuilder {
259 inner: ClawDBBuilder,
260}
261
262impl ClientBuilder {
263 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
265 self.inner = self.inner.endpoint(endpoint);
266 self
267 }
268
269 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 pub async fn build(self) -> SdkResult<ClawDBClient> {
277 Ok(ClawDBClient { inner: self.inner.build().await? })
278 }
279}
280
281pub struct MemoryNamespace {
283 client: ClawDB,
284}
285
286impl MemoryNamespace {
287 pub fn remember(&self, content: impl Into<String>) -> RememberCall {
289 RememberCall { client: self.client.clone(), content: content.into() }
290 }
291
292 pub fn search(&self, query: impl Into<String>) -> SearchCall {
294 SearchCall { client: self.client.clone(), query: query.into(), opts: SearchOptions::default() }
295 }
296}
297
298pub struct RememberCall {
300 client: ClawDB,
301 content: String,
302}
303
304impl RememberCall {
305 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
320pub struct SearchCall {
322 client: ClawDB,
323 query: String,
324 opts: SearchOptions,
325}
326
327impl SearchCall {
328 pub fn top_k(mut self, top_k: u32) -> Self {
330 self.opts.top_k = Some(top_k);
331 self
332 }
333
334 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}