1use std::fs;
2use std::path::PathBuf;
3use std::process::{Command, Stdio};
4use std::sync::Arc;
5use std::time::Duration;
6
7use dirs::home_dir;
8use reqwest::{Client as HttpClient, header};
9use serde_json::{json, Value};
10use tracing::info;
11
12use crate::builder::ClawDBBuilder;
13use crate::error::{SdkError, SdkResult};
14use crate::models::{
15 BranchInfo, DiffResult, HealthResponse, MemoryRecord, MergeResult, ReflectJob,
16 RememberOptions, SearchHit, SearchOptions, SessionInfo, SyncActionResult,
17 SyncResult, SyncStatusResult, TxInfo,
18};
19
20struct ClawDBInner {
23 endpoint: String,
24 api_key: String,
25 agent_id: String,
26 timeout_ms: u64,
27 http: HttpClient,
28}
29
30const LOCAL_ENDPOINT: &str = "http://localhost:50050";
31const LOCAL_SOCKET: &str = "127.0.0.1:50050";
32const DEFAULT_LOCAL_JWT_SECRET: &str = "clawdb-sdk-local-dev-secret";
33
34async fn local_server_healthy() -> bool {
35 tokio::net::TcpStream::connect(LOCAL_SOCKET).await.is_ok()
36}
37
38async fn wait_for_local_server(timeout: Duration) -> bool {
39 let started = tokio::time::Instant::now();
40 while started.elapsed() < timeout {
41 if local_server_healthy().await {
42 return true;
43 }
44 tokio::time::sleep(Duration::from_millis(100)).await;
45 }
46 false
47}
48
49fn installed_binary_path() -> Option<PathBuf> {
50 let binary = if cfg!(windows) {
51 "clawdb-server.exe"
52 } else {
53 "clawdb-server"
54 };
55
56 home_dir().map(|home| home.join(".clawdb").join("bin").join(binary))
57}
58
59fn persist_server_pid(pid: u32) {
60 if let Some(home) = home_dir() {
61 let claw_dir = home.join(".clawdb");
62 let _ = fs::create_dir_all(&claw_dir);
63 let _ = fs::write(claw_dir.join("server.pid"), pid.to_string());
64 }
65}
66
67fn spawn_local_server(binary: impl AsRef<std::ffi::OsStr>) -> bool {
68 let mut command = Command::new(binary);
69 command
70 .arg("--grpc-port")
71 .arg("50050")
72 .stdin(Stdio::null())
73 .stdout(Stdio::null())
74 .stderr(Stdio::null());
75 if std::env::var_os("CLAW_GUARD_JWT_SECRET").is_none() {
76 command.env("CLAW_GUARD_JWT_SECRET", DEFAULT_LOCAL_JWT_SECRET);
77 }
78 if std::env::var_os("CLAW_VECTOR_ENABLED").is_none() {
79 command.env("CLAW_VECTOR_ENABLED", "false");
80 }
81
82 match command.spawn() {
83 Ok(child) => {
84 persist_server_pid(child.id());
85 true
86 }
87 Err(_) => false,
88 }
89}
90
91async fn ensure_local_server_available() -> SdkResult<String> {
92 if local_server_healthy().await {
93 return Ok(LOCAL_ENDPOINT.into());
94 }
95
96 if spawn_local_server("clawdb-server") && wait_for_local_server(Duration::from_secs(5)).await {
97 return Ok(LOCAL_ENDPOINT.into());
98 }
99
100 if let Some(installed_binary) = installed_binary_path() {
101 if installed_binary.exists()
102 && spawn_local_server(&installed_binary)
103 && wait_for_local_server(Duration::from_secs(5)).await
104 {
105 return Ok(LOCAL_ENDPOINT.into());
106 }
107 }
108
109 Err(SdkError::Config(
110 "could not auto-provision clawdb-server; install clawdb-server or set CLAWDB_URL/CLAWDB_API_KEY".into(),
111 ))
112}
113
114#[derive(Clone)]
118pub struct ClawDB {
119 inner: Arc<ClawDBInner>,
120}
121
122impl ClawDB {
123 pub fn builder() -> ClawDBBuilder {
125 ClawDBBuilder::new()
126 }
127
128 pub async fn from_env() -> SdkResult<Self> {
130 ClawDBBuilder::from_env().build().await
131 }
132
133 pub async fn auto_provision() -> SdkResult<Self> {
135 if let Ok(endpoint) = std::env::var("CLAWDB_URL") {
136 return ClawDBBuilder::from_env().endpoint(endpoint).build().await;
137 }
138 if let Ok(api_key) = std::env::var("CLAWDB_API_KEY") {
139 return ClawDBBuilder::from_env()
140 .api_key(api_key)
141 .endpoint("https://cloud.clawdb.dev")
142 .build()
143 .await;
144 }
145 let endpoint = ensure_local_server_available().await?;
146
147 ClawDBBuilder::from_env()
148 .endpoint(endpoint)
149 .build()
150 .await
151 }
152
153 pub async fn from_api_key(api_key: impl Into<String>, endpoint: impl Into<String>) -> SdkResult<Self> {
155 ClawDBBuilder::new().api_key(api_key).endpoint(endpoint).build().await
156 }
157
158 pub(crate) async fn new_internal(
159 endpoint: String,
160 api_key: String,
161 agent_id: String,
162 _workspace: String,
163 _role: String,
164 timeout_ms: u64,
165 ) -> SdkResult<Self> {
166 let mut default_headers = header::HeaderMap::new();
167 if !api_key.is_empty() {
168 let val = header::HeaderValue::from_str(&format!("Bearer {}", api_key))
169 .map_err(|_| SdkError::Config("Invalid API key characters".into()))?;
170 default_headers.insert(header::AUTHORIZATION, val);
171 }
172 let http = HttpClient::builder()
173 .default_headers(default_headers)
174 .timeout(Duration::from_millis(timeout_ms))
175 .build()
176 .map_err(|e| SdkError::Config(e.to_string()))?;
177
178 Ok(Self {
179 inner: Arc::new(ClawDBInner { endpoint, api_key, agent_id, timeout_ms, http }),
180 })
181 }
182
183 async fn get(&self, path: &str) -> SdkResult<Value> {
186 let url = format!("{}{}", self.inner.endpoint, path);
187 let resp = self.inner.http.get(&url).send().await.map_err(SdkError::Reqwest)?;
188 self.parse_response(resp).await
189 }
190
191 async fn post(&self, path: &str, body: Value) -> SdkResult<Value> {
192 let url = format!("{}{}", self.inner.endpoint, path);
193 let resp = self.inner.http.post(&url).json(&body).send().await.map_err(SdkError::Reqwest)?;
194 self.parse_response(resp).await
195 }
196
197 async fn patch(&self, path: &str, body: Value) -> SdkResult<Value> {
198 let url = format!("{}{}", self.inner.endpoint, path);
199 let resp = self.inner.http.patch(&url).json(&body).send().await.map_err(SdkError::Reqwest)?;
200 self.parse_response(resp).await
201 }
202
203 async fn delete(&self, path: &str) -> SdkResult<Value> {
204 let url = format!("{}{}", self.inner.endpoint, path);
205 let resp = self.inner.http.delete(&url).send().await.map_err(SdkError::Reqwest)?;
206 self.parse_response(resp).await
207 }
208
209 async fn parse_response(&self, resp: reqwest::Response) -> SdkResult<Value> {
210 let status = resp.status().as_u16();
211 let text = resp.text().await.map_err(SdkError::Reqwest)?;
212 if status >= 400 {
213 return Err(SdkError::from_http(status, &text));
214 }
215 if text.is_empty() {
216 return Ok(Value::Null);
217 }
218 serde_json::from_str(&text).map_err(SdkError::Serialization)
219 }
220
221 fn str_field(v: &Value, field: &str) -> String {
222 v[field].as_str().unwrap_or_default().to_string()
223 }
224
225 fn extract_array<T: serde::de::DeserializeOwned>(v: &Value, field: &str) -> Vec<T> {
226 let arr = if v[field].is_array() { &v[field] } else { v };
227 serde_json::from_value(arr.clone()).unwrap_or_default()
228 }
229
230 pub async fn health(&self) -> SdkResult<HealthResponse> {
234 let v = self.get("/v1/health").await?;
235 Ok(HealthResponse {
236 status: Self::str_field(&v, "status"),
237 version: v["version"].as_str().map(str::to_string),
238 })
239 }
240
241 pub async fn ping(&self) -> SdkResult<()> {
243 self.health().await.map(|_| ())
244 }
245
246 pub async fn create_session(&self, role: &str, scopes: &[&str], ttl_secs: u64) -> SdkResult<SessionInfo> {
250 let v = self.post("/v1/sessions", json!({
251 "role": role,
252 "scopes": scopes,
253 "ttl_secs": ttl_secs,
254 })).await?;
255 Ok(SessionInfo {
256 session_id: Self::str_field(&v, "session_id"),
257 role: Self::str_field(&v, "role"),
258 scopes: v["scopes"].as_array()
259 .map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_string)).collect())
260 .unwrap_or_default(),
261 expires_at: None,
262 })
263 }
264
265 pub async fn validate_session(&self) -> SdkResult<SessionInfo> {
267 let v = self.get("/v1/sessions/me").await?;
268 Ok(SessionInfo {
269 session_id: Self::str_field(&v, "session_id"),
270 role: Self::str_field(&v, "role"),
271 scopes: v["scopes"].as_array()
272 .map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_string)).collect())
273 .unwrap_or_default(),
274 expires_at: None,
275 })
276 }
277
278 pub async fn revoke_session(&self, session_id: &str) -> SdkResult<bool> {
280 let v = self.delete(&format!("/v1/sessions/{}", session_id)).await?;
281 Ok(v["revoked"].as_bool().unwrap_or(true))
282 }
283
284 pub async fn active_session_count(&self) -> SdkResult<u64> {
286 let v = self.get("/v1/sessions/active/count").await?;
287 Ok(v["count"].as_u64().unwrap_or(0))
288 }
289
290 pub async fn remember(&self, content: impl Into<String>) -> SdkResult<String> {
294 let content = content.into();
295 if content.trim().is_empty() {
296 return Err(SdkError::Validation { field: "content".into(), constraint: "must be non-empty".into() });
297 }
298 let v = self.post("/v1/memories", json!({ "content": content })).await?;
299 Ok(Self::str_field(&v, "id"))
300 }
301
302 pub async fn remember_typed(&self, content: impl Into<String>, opts: RememberOptions) -> SdkResult<String> {
304 let content = content.into();
305 let mut body = json!({ "content": content });
306 if let Some(mt) = &opts.memory_type {
307 body["type"] = json!(mt);
308 }
309 if let Some(tags) = &opts.tags {
310 body["tags"] = json!(tags);
311 }
312 if let Some(ttl) = opts.ttl_days {
313 body["ttl_days"] = json!(ttl);
314 }
315 let v = self.post("/v1/memories", body).await?;
316 Ok(Self::str_field(&v, "id"))
317 }
318
319 pub async fn update_memory(&self, memory_id: &str, content: impl Into<String>) -> SdkResult<bool> {
321 let v = self.patch(&format!("/v1/memories/{}", memory_id), json!({ "content": content.into() })).await?;
322 Ok(v["updated"].as_bool().unwrap_or(true))
323 }
324
325 pub async fn search(&self, query: impl Into<String>, opts: SearchOptions) -> SdkResult<Vec<SearchHit>> {
327 let top_k = opts.top_k.unwrap_or(5);
328 let v = self.post("/v1/memories/search", json!({
329 "query": query.into(),
330 "top_k": top_k,
331 "semantic": opts.semantic.unwrap_or(true),
332 })).await?;
333 let hits: Vec<SearchHit> = Self::extract_array(&v, "hits");
334 Ok(hits)
335 }
336
337 pub async fn recall(&self, ids: &[&str]) -> SdkResult<Vec<MemoryRecord>> {
339 let v = self.post("/v1/memories/recall", json!({ "ids": ids })).await?;
340 let records: Vec<MemoryRecord> = Self::extract_array(&v, "memories");
341 Ok(records)
342 }
343
344 pub async fn list_memories(&self, memory_type: Option<&str>, limit: Option<u32>) -> SdkResult<Vec<MemoryRecord>> {
346 let mut path = "/v1/memories?".to_string();
347 if let Some(mt) = memory_type { path.push_str(&format!("type={}&", mt)); }
348 if let Some(l) = limit { path.push_str(&format!("limit={}", l)); }
349 let v = self.get(&path).await?;
350 let records: Vec<MemoryRecord> = Self::extract_array(&v, "memories");
351 Ok(records)
352 }
353
354 pub async fn delete_memory(&self, memory_id: &str) -> SdkResult<bool> {
356 let v = self.delete(&format!("/v1/memories/{}", memory_id)).await?;
357 Ok(v["deleted"].as_bool().unwrap_or(true))
358 }
359
360 pub async fn branch(&self, name: &str, from: Option<&str>) -> SdkResult<BranchInfo> {
364 let mut body = json!({ "name": name });
365 if let Some(f) = from { body["from_branch_id"] = json!(f); }
366 let v = self.post("/v1/branches", body).await?;
367 Ok(BranchInfo {
368 branch_id: Self::str_field(&v, "branch_id"),
369 name: Self::str_field(&v, "name"),
370 branch_json: v["branch_json"].as_str().map(str::to_string),
371 })
372 }
373
374 pub async fn list_branches(&self) -> SdkResult<Vec<BranchInfo>> {
376 let v = self.get("/v1/branches").await?;
377 let branches: Vec<BranchInfo> = Self::extract_array(&v, "branches");
378 Ok(branches)
379 }
380
381 pub async fn get_branch(&self, branch_id: &str) -> SdkResult<BranchInfo> {
383 let v = self.get(&format!("/v1/branches/{}", branch_id)).await?;
384 Ok(BranchInfo {
385 branch_id: Self::str_field(&v, "branch_id"),
386 name: Self::str_field(&v, "name"),
387 branch_json: v["branch_json"].as_str().map(str::to_string),
388 })
389 }
390
391 pub async fn get_branch_by_name(&self, name: &str) -> SdkResult<BranchInfo> {
393 let v = self.get(&format!("/v1/branches/by-name/{}", name)).await?;
394 Ok(BranchInfo {
395 branch_id: Self::str_field(&v, "branch_id"),
396 name: Self::str_field(&v, "name"),
397 branch_json: v["branch_json"].as_str().map(str::to_string),
398 })
399 }
400
401 pub async fn get_trunk_branch(&self) -> SdkResult<BranchInfo> {
403 let v = self.get("/v1/branches/trunk").await?;
404 Ok(BranchInfo {
405 branch_id: Self::str_field(&v, "branch_id"),
406 name: Self::str_field(&v, "name"),
407 branch_json: v["branch_json"].as_str().map(str::to_string),
408 })
409 }
410
411 pub async fn diff(&self, source_branch_id: &str, target_branch_id: &str) -> SdkResult<DiffResult> {
413 let v = self.get(&format!("/v1/branches/{}/diff?target={}", source_branch_id, target_branch_id)).await?;
414 Ok(DiffResult {
415 added: v["added"].as_u64().unwrap_or(0) as u32,
416 removed: v["removed"].as_u64().unwrap_or(0) as u32,
417 modified: v["modified"].as_u64().unwrap_or(0) as u32,
418 unchanged: v["unchanged"].as_u64().unwrap_or(0) as u32,
419 divergence_score: v["divergence_score"].as_f64().unwrap_or(0.0),
420 diff_json: v["diff_json"].as_str().map(str::to_string),
421 })
422 }
423
424 pub async fn merge(&self, source_branch_id: &str, target_branch_id: &str, strategy: &str) -> SdkResult<MergeResult> {
426 let v = self.post(&format!("/v1/branches/{}/merge", source_branch_id), json!({
427 "target_branch_id": target_branch_id,
428 "strategy": strategy,
429 })).await?;
430 Ok(MergeResult {
431 success: v["success"].as_bool().unwrap_or(true),
432 applied: v["applied"].as_u64().unwrap_or(0) as u32,
433 skipped: v["skipped"].as_u64().unwrap_or(0) as u32,
434 conflicts: v["conflicts"].as_u64().unwrap_or(0) as u32,
435 duration_ms: v["duration_ms"].as_u64().unwrap_or(0),
436 })
437 }
438
439 pub async fn discard_branch(&self, branch_id: &str) -> SdkResult<bool> {
441 let v = self.delete(&format!("/v1/branches/{}", branch_id)).await?;
442 Ok(v["discarded"].as_bool().unwrap_or(true))
443 }
444
445 pub async fn archive_branch(&self, branch_id: &str) -> SdkResult<bool> {
447 let v = self.post(&format!("/v1/branches/{}/archive", branch_id), json!({})).await?;
448 Ok(v["archived"].as_bool().unwrap_or(true))
449 }
450
451 pub async fn sync(&self) -> SdkResult<SyncResult> {
455 let v = self.post("/v1/sync", json!({})).await?;
456 Ok(SyncResult {
457 pushed: v["pushed"].as_u64().unwrap_or(0) as u32,
458 pulled: v["pulled"].as_u64().unwrap_or(0) as u32,
459 conflicts: v["conflicts"].as_u64().unwrap_or(0) as u32,
460 duration_ms: v["duration_ms"].as_u64().unwrap_or(0),
461 })
462 }
463
464 pub async fn push_sync(&self) -> SdkResult<SyncActionResult> {
466 let v = self.post("/v1/sync/push", json!({})).await?;
467 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
468 }
469
470 pub async fn pull_sync(&self) -> SdkResult<SyncActionResult> {
472 let v = self.post("/v1/sync/pull", json!({})).await?;
473 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
474 }
475
476 pub async fn reconcile_sync(&self) -> SdkResult<SyncActionResult> {
478 let v = self.post("/v1/sync/reconcile", json!({})).await?;
479 Ok(SyncActionResult { summary_json: Some(v.to_string()) })
480 }
481
482 pub async fn sync_status(&self) -> SdkResult<SyncStatusResult> {
484 let v = self.get("/v1/sync/status").await?;
485 Ok(SyncStatusResult { status_json: Some(v.to_string()) })
486 }
487
488 pub async fn reflect(&self) -> SdkResult<ReflectJob> {
492 let v = self.post("/v1/reflect", json!({ "agent_id": self.inner.agent_id })).await?;
493 Ok(ReflectJob {
494 job_id: Self::str_field(&v, "job_id"),
495 status: Self::str_field(&v, "status"),
496 message: v["message"].as_str().map(str::to_string),
497 skipped: v["skipped"].as_bool().unwrap_or(false),
498 })
499 }
500
501 pub async fn reflect_list_jobs(&self, agent_id: &str) -> SdkResult<Vec<ReflectJob>> {
503 let v = self.get(&format!("/v1/reflect/jobs?agent_id={}", agent_id)).await?;
504 let jobs: Vec<ReflectJob> = Self::extract_array(&v, "jobs");
505 Ok(jobs)
506 }
507
508 pub async fn reflect_get_job(&self, job_id: &str) -> SdkResult<ReflectJob> {
510 let v = self.get(&format!("/v1/reflect/jobs/{}", job_id)).await?;
511 Ok(ReflectJob {
512 job_id: Self::str_field(&v, "job_id"),
513 status: Self::str_field(&v, "status"),
514 message: v["message"].as_str().map(str::to_string),
515 skipped: v["skipped"].as_bool().unwrap_or(false),
516 })
517 }
518
519 pub async fn reflect_get_facts(&self, agent_id: &str) -> SdkResult<Value> {
521 self.get(&format!("/v1/reflect/facts/{}", agent_id)).await
522 }
523
524 pub async fn reflect_get_preferences(&self, agent_id: &str) -> SdkResult<Value> {
526 self.get(&format!("/v1/reflect/preferences/{}", agent_id)).await
527 }
528
529 pub async fn reflect_get_contradictions(&self, agent_id: &str) -> SdkResult<Value> {
531 self.get(&format!("/v1/reflect/contradictions/{}", agent_id)).await
532 }
533
534 pub async fn reflect_resolve_contradiction(
536 &self,
537 agent_id: &str,
538 contradiction_id: &str,
539 strategy: &str,
540 merged_value_json: Option<&str>,
541 ) -> SdkResult<Value> {
542 self.post(
543 &format!("/v1/reflect/contradictions/{}/{}/resolve", agent_id, contradiction_id),
544 json!({ "strategy": strategy, "merged_value_json": merged_value_json }),
545 ).await
546 }
547
548 pub async fn begin_tx(&self) -> SdkResult<TxInfo> {
552 let v = self.post("/v1/tx", json!({})).await?;
553 Ok(TxInfo { tx_id: Self::str_field(&v, "tx_id") })
554 }
555
556 pub async fn tx_remember(&self, tx_id: &str, content: impl Into<String>) -> SdkResult<String> {
558 let v = self.post(&format!("/v1/tx/{}/memories", tx_id), json!({ "content": content.into() })).await?;
559 Ok(Self::str_field(&v, "id"))
560 }
561
562 pub async fn tx_remember_typed(&self, tx_id: &str, content: impl Into<String>, opts: RememberOptions) -> SdkResult<String> {
564 let mut body = json!({ "content": content.into() });
565 if let Some(mt) = &opts.memory_type { body["type"] = json!(mt); }
566 if let Some(tags) = &opts.tags { body["tags"] = json!(tags); }
567 let v = self.post(&format!("/v1/tx/{}/memories/typed", tx_id), body).await?;
568 Ok(Self::str_field(&v, "id"))
569 }
570
571 pub async fn commit_tx(&self, tx_id: &str) -> SdkResult<bool> {
573 let v = self.post(&format!("/v1/tx/{}/commit", tx_id), json!({})).await?;
574 Ok(v["committed"].as_bool().unwrap_or(true))
575 }
576
577 pub async fn rollback_tx(&self, tx_id: &str) -> SdkResult<bool> {
579 let v = self.post(&format!("/v1/tx/{}/rollback", tx_id), json!({})).await?;
580 Ok(v["rolled_back"].as_bool().unwrap_or(true))
581 }
582
583 pub fn close(&self) {
585 info!("clawdb.close");
586 }
587
588 pub fn endpoint(&self) -> &str {
590 &self.inner.endpoint
591 }
592
593 pub fn agent_id(&self) -> &str {
595 &self.inner.agent_id
596 }
597}
598
599#[derive(Clone)]
603pub struct ClawDBClient {
604 pub(crate) inner: ClawDB,
605}
606
607impl ClawDBClient {
608 pub async fn auto_provision() -> SdkResult<Self> {
609 Ok(Self { inner: ClawDB::auto_provision().await? })
610 }
611
612 pub fn builder() -> crate::builder::ClawDBBuilder {
613 ClawDB::builder()
614 }
615}
616
617impl std::ops::Deref for ClawDBClient {
618 type Target = ClawDB;
619 fn deref(&self) -> &Self::Target {
620 &self.inner
621 }
622}
623
624impl std::fmt::Debug for ClawDB {
625 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626 f.debug_struct("ClawDB")
627 .field("endpoint", &self.inner.endpoint)
628 .field("agent_id", &self.inner.agent_id)
629 .finish()
630 }
631}
632
633impl std::fmt::Debug for ClawDBClient {
634 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635 f.debug_struct("ClawDBClient")
636 .field("inner", &self.inner)
637 .finish()
638 }
639}