Skip to main content

clawdb_client/
client.rs

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
20// ─── Internal shared state ────────────────────────────────────────────────────
21
22struct 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// ─── ClawDB client ─────────────────────────────────────────────────────────
115
116/// The primary ClawDB client.
117#[derive(Clone)]
118pub struct ClawDB {
119    inner: Arc<ClawDBInner>,
120}
121
122impl ClawDB {
123    /// Create a builder to configure and construct a ClawDB client.
124    pub fn builder() -> ClawDBBuilder {
125        ClawDBBuilder::new()
126    }
127
128    /// Create a client from environment variables.
129    pub async fn from_env() -> SdkResult<Self> {
130        ClawDBBuilder::from_env().build().await
131    }
132
133    /// Automatically provision a usable endpoint following the SDK fallback order.
134    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    /// Create a client from an API key and endpoint.
154    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    // ─── HTTP helpers ────────────────────────────────────────────────────
184
185    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    // ─── Health ──────────────────────────────────────────────────────────
231
232    /// Check server health.
233    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    /// Ping the server (returns Ok if reachable).
242    pub async fn ping(&self) -> SdkResult<()> {
243        self.health().await.map(|_| ())
244    }
245
246    // ─── Sessions ────────────────────────────────────────────────────────
247
248    /// Create a new session token.
249    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    /// Validate the current session.
266    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    /// Revoke a session by ID.
279    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    /// Get the count of active sessions.
285    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    // ─── Memory ──────────────────────────────────────────────────────────
291
292    /// Store a plain memory and return its ID.
293    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    /// Store a typed memory and return its ID.
303    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    /// Update an existing memory's content.
320    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    /// Semantic search over memories.
326    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    /// Recall specific memories by ID.
338    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    /// List memories with optional type filter.
345    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    /// Delete a memory by ID.
355    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    // ─── Branches ────────────────────────────────────────────────────────
361
362    /// Fork a new branch.
363    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    /// List all branches.
375    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    /// Get a branch by ID.
382    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    /// Get a branch by name.
392    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    /// Get the trunk (main) branch.
402    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    /// Diff a branch against a target.
412    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    /// Merge source branch into target.
425    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    /// Discard (delete) a branch.
440    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    /// Archive a branch.
446    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    // ─── Sync ────────────────────────────────────────────────────────────
452
453    /// Full bidirectional sync.
454    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    /// Push local memories to remote.
465    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    /// Pull remote memories to local.
471    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    /// Reconcile divergent sync state.
477    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    /// Get current sync status.
483    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    // ─── Reflect ─────────────────────────────────────────────────────────
489
490    /// Trigger a new reflection job.
491    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    /// List reflection jobs.
502    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    /// Get a specific reflection job.
509    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    /// Get extracted facts for an agent.
520    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    /// Get preferences for an agent.
525    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    /// Get contradictions for an agent.
530    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    /// Resolve a specific contradiction.
535    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    // ─── Transactions ─────────────────────────────────────────────────────
549
550    /// Begin a new transaction.
551    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    /// Add a plain memory to a transaction.
557    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    /// Add a typed memory to a transaction.
563    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    /// Commit a transaction.
572    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    /// Roll back a transaction.
578    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    /// Close the client (no-op for HTTP).
584    pub fn close(&self) {
585        info!("clawdb.close");
586    }
587
588    /// Return the configured endpoint.
589    pub fn endpoint(&self) -> &str {
590        &self.inner.endpoint
591    }
592
593    /// Return the configured agent_id.
594    pub fn agent_id(&self) -> &str {
595        &self.inner.agent_id
596    }
597}
598
599// ─── ClawDBClient alias (legacy compat) ───────────────────────────────────
600
601/// Alias for [`ClawDB`] for backwards compatibility.
602#[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}