1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::{Client as HttpClient, header};
5use serde_json::{json, Value};
6use tracing::info;
7
8use crate::builder::ClawDBBuilder;
9use crate::error::{SdkError, SdkResult};
10use crate::models::{
11 BranchInfo, DiffResult, HealthResponse, MemoryRecord, MergeResult, ReflectJob,
12 RememberOptions, SearchHit, SearchOptions, SessionInfo, SyncActionResult,
13 SyncResult, SyncStatusResult, TxInfo,
14};
15
16struct ClawDBInner {
19 endpoint: String,
20 api_key: String,
21 agent_id: String,
22 timeout_ms: u64,
23 http: HttpClient,
24}
25
26#[derive(Clone)]
30pub struct ClawDB {
31 inner: Arc<ClawDBInner>,
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 if let Ok(api_key) = std::env::var("CLAWDB_API_KEY") {
51 return ClawDBBuilder::from_env()
52 .api_key(api_key)
53 .endpoint("https://cloud.clawdb.dev")
54 .build()
55 .await;
56 }
57 if tokio::net::TcpStream::connect("127.0.0.1:50050").await.is_ok() {
58 return ClawDBBuilder::from_env()
59 .endpoint("http://localhost:50050")
60 .build()
61 .await;
62 }
63 Err(SdkError::Config("could not auto-provision clawdb-server".into()))
64 }
65
66 pub async fn from_api_key(api_key: impl Into<String>, endpoint: impl Into<String>) -> SdkResult<Self> {
68 ClawDBBuilder::new().api_key(api_key).endpoint(endpoint).build().await
69 }
70
71 pub(crate) async fn new_internal(
72 endpoint: String,
73 api_key: String,
74 agent_id: String,
75 _workspace: String,
76 _role: String,
77 timeout_ms: u64,
78 ) -> SdkResult<Self> {
79 let mut default_headers = header::HeaderMap::new();
80 if !api_key.is_empty() {
81 let val = header::HeaderValue::from_str(&format!("Bearer {}", api_key))
82 .map_err(|_| SdkError::Config("Invalid API key characters".into()))?;
83 default_headers.insert(header::AUTHORIZATION, val);
84 }
85 let http = HttpClient::builder()
86 .default_headers(default_headers)
87 .timeout(Duration::from_millis(timeout_ms))
88 .build()
89 .map_err(|e| SdkError::Config(e.to_string()))?;
90
91 Ok(Self {
92 inner: Arc::new(ClawDBInner { endpoint, api_key, agent_id, timeout_ms, http }),
93 })
94 }
95
96 async fn get(&self, path: &str) -> SdkResult<Value> {
99 let url = format!("{}{}", self.inner.endpoint, path);
100 let resp = self.inner.http.get(&url).send().await.map_err(SdkError::Reqwest)?;
101 self.parse_response(resp).await
102 }
103
104 async fn post(&self, path: &str, body: Value) -> SdkResult<Value> {
105 let url = format!("{}{}", self.inner.endpoint, path);
106 let resp = self.inner.http.post(&url).json(&body).send().await.map_err(SdkError::Reqwest)?;
107 self.parse_response(resp).await
108 }
109
110 async fn patch(&self, path: &str, body: Value) -> SdkResult<Value> {
111 let url = format!("{}{}", self.inner.endpoint, path);
112 let resp = self.inner.http.patch(&url).json(&body).send().await.map_err(SdkError::Reqwest)?;
113 self.parse_response(resp).await
114 }
115
116 async fn delete(&self, path: &str) -> SdkResult<Value> {
117 let url = format!("{}{}", self.inner.endpoint, path);
118 let resp = self.inner.http.delete(&url).send().await.map_err(SdkError::Reqwest)?;
119 self.parse_response(resp).await
120 }
121
122 async fn parse_response(&self, resp: reqwest::Response) -> SdkResult<Value> {
123 let status = resp.status().as_u16();
124 let text = resp.text().await.map_err(SdkError::Reqwest)?;
125 if status >= 400 {
126 return Err(SdkError::from_http(status, &text));
127 }
128 if text.is_empty() {
129 return Ok(Value::Null);
130 }
131 serde_json::from_str(&text).map_err(SdkError::Serialization)
132 }
133
134 fn str_field(v: &Value, field: &str) -> String {
135 v[field].as_str().unwrap_or_default().to_string()
136 }
137
138 fn extract_array<T: serde::de::DeserializeOwned>(v: &Value, field: &str) -> Vec<T> {
139 let arr = if v[field].is_array() { &v[field] } else { v };
140 serde_json::from_value(arr.clone()).unwrap_or_default()
141 }
142
143 pub async fn health(&self) -> SdkResult<HealthResponse> {
147 let v = self.get("/v1/health").await?;
148 Ok(HealthResponse {
149 status: Self::str_field(&v, "status"),
150 version: v["version"].as_str().map(str::to_string),
151 })
152 }
153
154 pub async fn ping(&self) -> SdkResult<()> {
156 self.health().await.map(|_| ())
157 }
158
159 pub async fn create_session(&self, role: &str, scopes: &[&str], ttl_secs: u64) -> SdkResult<SessionInfo> {
163 let v = self.post("/v1/sessions", json!({
164 "role": role,
165 "scopes": scopes,
166 "ttl_secs": ttl_secs,
167 })).await?;
168 Ok(SessionInfo {
169 session_id: Self::str_field(&v, "session_id"),
170 role: Self::str_field(&v, "role"),
171 scopes: v["scopes"].as_array()
172 .map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_string)).collect())
173 .unwrap_or_default(),
174 expires_at: None,
175 })
176 }
177
178 pub async fn validate_session(&self) -> SdkResult<SessionInfo> {
180 let v = self.get("/v1/sessions/me").await?;
181 Ok(SessionInfo {
182 session_id: Self::str_field(&v, "session_id"),
183 role: Self::str_field(&v, "role"),
184 scopes: v["scopes"].as_array()
185 .map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_string)).collect())
186 .unwrap_or_default(),
187 expires_at: None,
188 })
189 }
190
191 pub async fn revoke_session(&self, session_id: &str) -> SdkResult<bool> {
193 let v = self.delete(&format!("/v1/sessions/{}", session_id)).await?;
194 Ok(v["revoked"].as_bool().unwrap_or(true))
195 }
196
197 pub async fn active_session_count(&self) -> SdkResult<u64> {
199 let v = self.get("/v1/sessions/active/count").await?;
200 Ok(v["count"].as_u64().unwrap_or(0))
201 }
202
203 pub async fn remember(&self, content: impl Into<String>) -> SdkResult<String> {
207 let content = content.into();
208 if content.trim().is_empty() {
209 return Err(SdkError::Validation { field: "content".into(), constraint: "must be non-empty".into() });
210 }
211 let v = self.post("/v1/memories", json!({ "content": content })).await?;
212 Ok(Self::str_field(&v, "id"))
213 }
214
215 pub async fn remember_typed(&self, content: impl Into<String>, opts: RememberOptions) -> SdkResult<String> {
217 let content = content.into();
218 let mut body = json!({ "content": content });
219 if let Some(mt) = &opts.memory_type {
220 body["type"] = json!(mt);
221 }
222 if let Some(tags) = &opts.tags {
223 body["tags"] = json!(tags);
224 }
225 if let Some(ttl) = opts.ttl_days {
226 body["ttl_days"] = json!(ttl);
227 }
228 let v = self.post("/v1/memories", body).await?;
229 Ok(Self::str_field(&v, "id"))
230 }
231
232 pub async fn update_memory(&self, memory_id: &str, content: impl Into<String>) -> SdkResult<bool> {
234 let v = self.patch(&format!("/v1/memories/{}", memory_id), json!({ "content": content.into() })).await?;
235 Ok(v["updated"].as_bool().unwrap_or(true))
236 }
237
238 pub async fn search(&self, query: impl Into<String>, opts: SearchOptions) -> SdkResult<Vec<SearchHit>> {
240 let top_k = opts.top_k.unwrap_or(5);
241 let v = self.post("/v1/memories/search", json!({
242 "query": query.into(),
243 "top_k": top_k,
244 "semantic": opts.semantic.unwrap_or(true),
245 })).await?;
246 let hits: Vec<SearchHit> = Self::extract_array(&v, "hits");
247 Ok(hits)
248 }
249
250 pub async fn recall(&self, ids: &[&str]) -> SdkResult<Vec<MemoryRecord>> {
252 let v = self.post("/v1/memories/recall", json!({ "ids": ids })).await?;
253 let records: Vec<MemoryRecord> = Self::extract_array(&v, "memories");
254 Ok(records)
255 }
256
257 pub async fn list_memories(&self, memory_type: Option<&str>, limit: Option<u32>) -> SdkResult<Vec<MemoryRecord>> {
259 let mut path = "/v1/memories?".to_string();
260 if let Some(mt) = memory_type { path.push_str(&format!("type={}&", mt)); }
261 if let Some(l) = limit { path.push_str(&format!("limit={}", l)); }
262 let v = self.get(&path).await?;
263 let records: Vec<MemoryRecord> = Self::extract_array(&v, "memories");
264 Ok(records)
265 }
266
267 pub async fn delete_memory(&self, memory_id: &str) -> SdkResult<bool> {
269 let v = self.delete(&format!("/v1/memories/{}", memory_id)).await?;
270 Ok(v["deleted"].as_bool().unwrap_or(true))
271 }
272
273 pub async fn branch(&self, name: &str, from: Option<&str>) -> SdkResult<BranchInfo> {
277 let mut body = json!({ "name": name });
278 if let Some(f) = from { body["from_branch_id"] = json!(f); }
279 let v = self.post("/v1/branches", body).await?;
280 Ok(BranchInfo {
281 branch_id: Self::str_field(&v, "branch_id"),
282 name: Self::str_field(&v, "name"),
283 branch_json: v["branch_json"].as_str().map(str::to_string),
284 })
285 }
286
287 pub async fn list_branches(&self) -> SdkResult<Vec<BranchInfo>> {
289 let v = self.get("/v1/branches").await?;
290 let branches: Vec<BranchInfo> = Self::extract_array(&v, "branches");
291 Ok(branches)
292 }
293
294 pub async fn get_branch(&self, branch_id: &str) -> SdkResult<BranchInfo> {
296 let v = self.get(&format!("/v1/branches/{}", branch_id)).await?;
297 Ok(BranchInfo {
298 branch_id: Self::str_field(&v, "branch_id"),
299 name: Self::str_field(&v, "name"),
300 branch_json: v["branch_json"].as_str().map(str::to_string),
301 })
302 }
303
304 pub async fn get_branch_by_name(&self, name: &str) -> SdkResult<BranchInfo> {
306 let v = self.get(&format!("/v1/branches/by-name/{}", name)).await?;
307 Ok(BranchInfo {
308 branch_id: Self::str_field(&v, "branch_id"),
309 name: Self::str_field(&v, "name"),
310 branch_json: v["branch_json"].as_str().map(str::to_string),
311 })
312 }
313
314 pub async fn get_trunk_branch(&self) -> SdkResult<BranchInfo> {
316 let v = self.get("/v1/branches/trunk").await?;
317 Ok(BranchInfo {
318 branch_id: Self::str_field(&v, "branch_id"),
319 name: Self::str_field(&v, "name"),
320 branch_json: v["branch_json"].as_str().map(str::to_string),
321 })
322 }
323
324 pub async fn diff(&self, source_branch_id: &str, target_branch_id: &str) -> SdkResult<DiffResult> {
326 let v = self.get(&format!("/v1/branches/{}/diff?target={}", source_branch_id, target_branch_id)).await?;
327 Ok(DiffResult {
328 added: v["added"].as_u64().unwrap_or(0) as u32,
329 removed: v["removed"].as_u64().unwrap_or(0) as u32,
330 modified: v["modified"].as_u64().unwrap_or(0) as u32,
331 unchanged: v["unchanged"].as_u64().unwrap_or(0) as u32,
332 divergence_score: v["divergence_score"].as_f64().unwrap_or(0.0),
333 diff_json: v["diff_json"].as_str().map(str::to_string),
334 })
335 }
336
337 pub async fn merge(&self, source_branch_id: &str, target_branch_id: &str, strategy: &str) -> SdkResult<MergeResult> {
339 let v = self.post(&format!("/v1/branches/{}/merge", source_branch_id), json!({
340 "target_branch_id": target_branch_id,
341 "strategy": strategy,
342 })).await?;
343 Ok(MergeResult {
344 success: v["success"].as_bool().unwrap_or(true),
345 applied: v["applied"].as_u64().unwrap_or(0) as u32,
346 skipped: v["skipped"].as_u64().unwrap_or(0) as u32,
347 conflicts: v["conflicts"].as_u64().unwrap_or(0) as u32,
348 duration_ms: v["duration_ms"].as_u64().unwrap_or(0),
349 })
350 }
351
352 pub async fn discard_branch(&self, branch_id: &str) -> SdkResult<bool> {
354 let v = self.delete(&format!("/v1/branches/{}", branch_id)).await?;
355 Ok(v["discarded"].as_bool().unwrap_or(true))
356 }
357
358 pub async fn archive_branch(&self, branch_id: &str) -> SdkResult<bool> {
360 let v = self.post(&format!("/v1/branches/{}/archive", branch_id), json!({})).await?;
361 Ok(v["archived"].as_bool().unwrap_or(true))
362 }
363
364 pub async fn sync(&self) -> SdkResult<SyncResult> {
368 let v = self.post("/v1/sync", json!({})).await?;
369 Ok(SyncResult {
370 pushed: v["pushed"].as_u64().unwrap_or(0) as u32,
371 pulled: v["pulled"].as_u64().unwrap_or(0) as u32,
372 conflicts: v["conflicts"].as_u64().unwrap_or(0) as u32,
373 duration_ms: v["duration_ms"].as_u64().unwrap_or(0),
374 })
375 }
376
377 pub async fn push_sync(&self) -> SdkResult<SyncActionResult> {
379 let v = self.post("/v1/sync/push", json!({})).await?;
380 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
381 }
382
383 pub async fn pull_sync(&self) -> SdkResult<SyncActionResult> {
385 let v = self.post("/v1/sync/pull", json!({})).await?;
386 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
387 }
388
389 pub async fn reconcile_sync(&self) -> SdkResult<SyncActionResult> {
391 let v = self.post("/v1/sync/reconcile", json!({})).await?;
392 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
393 }
394
395 pub async fn sync_status(&self) -> SdkResult<SyncStatusResult> {
397 let v = self.get("/v1/sync/status").await?;
398 Ok(SyncStatusResult { status_json: Some(v.to_string()) })
399 }
400
401 pub async fn reflect(&self) -> SdkResult<ReflectJob> {
405 let v = self.post("/v1/reflect", json!({ "agent_id": self.inner.agent_id })).await?;
406 Ok(ReflectJob {
407 job_id: Self::str_field(&v, "job_id"),
408 status: Self::str_field(&v, "status"),
409 message: v["message"].as_str().map(str::to_string),
410 skipped: v["skipped"].as_bool().unwrap_or(false),
411 })
412 }
413
414 pub async fn reflect_list_jobs(&self, agent_id: &str) -> SdkResult<Vec<ReflectJob>> {
416 let v = self.get(&format!("/v1/reflect/jobs?agent_id={}", agent_id)).await?;
417 let jobs: Vec<ReflectJob> = Self::extract_array(&v, "jobs");
418 Ok(jobs)
419 }
420
421 pub async fn reflect_get_job(&self, job_id: &str) -> SdkResult<ReflectJob> {
423 let v = self.get(&format!("/v1/reflect/jobs/{}", job_id)).await?;
424 Ok(ReflectJob {
425 job_id: Self::str_field(&v, "job_id"),
426 status: Self::str_field(&v, "status"),
427 message: v["message"].as_str().map(str::to_string),
428 skipped: v["skipped"].as_bool().unwrap_or(false),
429 })
430 }
431
432 pub async fn reflect_get_facts(&self, agent_id: &str) -> SdkResult<Value> {
434 self.get(&format!("/v1/reflect/facts/{}", agent_id)).await
435 }
436
437 pub async fn reflect_get_preferences(&self, agent_id: &str) -> SdkResult<Value> {
439 self.get(&format!("/v1/reflect/preferences/{}", agent_id)).await
440 }
441
442 pub async fn reflect_get_contradictions(&self, agent_id: &str) -> SdkResult<Value> {
444 self.get(&format!("/v1/reflect/contradictions/{}", agent_id)).await
445 }
446
447 pub async fn reflect_resolve_contradiction(
449 &self,
450 agent_id: &str,
451 contradiction_id: &str,
452 strategy: &str,
453 merged_value_json: Option<&str>,
454 ) -> SdkResult<Value> {
455 self.post(
456 &format!("/v1/reflect/contradictions/{}/{}/resolve", agent_id, contradiction_id),
457 json!({ "strategy": strategy, "merged_value_json": merged_value_json }),
458 ).await
459 }
460
461 pub async fn begin_tx(&self) -> SdkResult<TxInfo> {
465 let v = self.post("/v1/tx", json!({})).await?;
466 Ok(TxInfo { tx_id: Self::str_field(&v, "tx_id") })
467 }
468
469 pub async fn tx_remember(&self, tx_id: &str, content: impl Into<String>) -> SdkResult<String> {
471 let v = self.post(&format!("/v1/tx/{}/memories", tx_id), json!({ "content": content.into() })).await?;
472 Ok(Self::str_field(&v, "id"))
473 }
474
475 pub async fn tx_remember_typed(&self, tx_id: &str, content: impl Into<String>, opts: RememberOptions) -> SdkResult<String> {
477 let mut body = json!({ "content": content.into() });
478 if let Some(mt) = &opts.memory_type { body["type"] = json!(mt); }
479 if let Some(tags) = &opts.tags { body["tags"] = json!(tags); }
480 let v = self.post(&format!("/v1/tx/{}/memories/typed", tx_id), body).await?;
481 Ok(Self::str_field(&v, "id"))
482 }
483
484 pub async fn commit_tx(&self, tx_id: &str) -> SdkResult<bool> {
486 let v = self.post(&format!("/v1/tx/{}/commit", tx_id), json!({})).await?;
487 Ok(v["committed"].as_bool().unwrap_or(true))
488 }
489
490 pub async fn rollback_tx(&self, tx_id: &str) -> SdkResult<bool> {
492 let v = self.post(&format!("/v1/tx/{}/rollback", tx_id), json!({})).await?;
493 Ok(v["rolled_back"].as_bool().unwrap_or(true))
494 }
495
496 pub fn close(&self) {
498 info!("clawdb.close");
499 }
500
501 pub fn endpoint(&self) -> &str {
503 &self.inner.endpoint
504 }
505
506 pub fn agent_id(&self) -> &str {
508 &self.inner.agent_id
509 }
510}
511
512#[derive(Clone)]
516pub struct ClawDBClient {
517 pub(crate) inner: ClawDB,
518}
519
520impl ClawDBClient {
521 pub async fn auto_provision() -> SdkResult<Self> {
522 Ok(Self { inner: ClawDB::auto_provision().await? })
523 }
524
525 pub fn builder() -> crate::builder::ClawDBBuilder {
526 ClawDB::builder()
527 }
528}
529
530impl std::ops::Deref for ClawDBClient {
531 type Target = ClawDB;
532 fn deref(&self) -> &Self::Target {
533 &self.inner
534 }
535}
536
537impl std::fmt::Debug for ClawDB {
538 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539 f.debug_struct("ClawDB")
540 .field("endpoint", &self.inner.endpoint)
541 .field("agent_id", &self.inner.agent_id)
542 .finish()
543 }
544}
545
546impl std::fmt::Debug for ClawDBClient {
547 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548 f.debug_struct("ClawDBClient")
549 .field("inner", &self.inner)
550 .finish()
551 }
552}