agtrace_sdk/
client.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::error::{Error, Result};
5use crate::types::*;
6use crate::watch::WatchBuilder;
7
8// ============================================================================
9// ClientBuilder
10// ============================================================================
11
12/// Builder for configuring and connecting to an agtrace workspace.
13///
14/// Provides flexible path resolution with the following priority:
15/// 1. Explicit path via `builder.path()`
16/// 2. `AGTRACE_PATH` environment variable
17/// 3. XDG data directory (e.g., `~/.local/share/agtrace` on Linux, `~/Library/Application Support/agtrace` on macOS)
18///
19/// # Examples
20///
21/// ```no_run
22/// # use agtrace_sdk::Client;
23/// # #[tokio::main]
24/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
25/// // Use default XDG path
26/// let client = Client::connect_default().await?;
27///
28/// // Use explicit path
29/// let client = Client::builder()
30///     .path("/custom/path")
31///     .connect().await?;
32///
33/// // Use AGTRACE_PATH environment variable
34/// // $ export AGTRACE_PATH=/tmp/agtrace
35/// let client = Client::builder().connect().await?;
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Default)]
40pub struct ClientBuilder {
41    path: Option<PathBuf>,
42}
43
44impl ClientBuilder {
45    /// Create a new ClientBuilder with default settings.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set an explicit workspace path (highest priority).
51    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
52        self.path = Some(path.into());
53        self
54    }
55
56    /// Connect to the workspace using the configured or resolved path.
57    /// If the workspace does not exist, it will be automatically initialized.
58    pub async fn connect(self) -> Result<Client> {
59        let path = self.resolve_path()?;
60        let runtime = agtrace_runtime::AgTrace::connect_or_create(path)
61            .await
62            .map_err(Error::Runtime)?;
63        Ok(Client {
64            inner: Arc::new(runtime),
65        })
66    }
67
68    /// Resolve the workspace path based on priority:
69    /// 1. Explicit path from builder
70    /// 2. AGTRACE_PATH environment variable
71    /// 3. XDG data directory
72    fn resolve_path(&self) -> Result<PathBuf> {
73        let explicit_path = self.path.as_ref().and_then(|p| p.to_str());
74        agtrace_runtime::resolve_workspace_path(explicit_path).map_err(Error::Runtime)
75    }
76}
77
78// ============================================================================
79// Main Client
80// ============================================================================
81
82/// Main entry point for interacting with an agtrace workspace.
83pub struct Client {
84    inner: Arc<agtrace_runtime::AgTrace>,
85}
86
87impl Client {
88    /// Create a new ClientBuilder for configuring workspace connection.
89    ///
90    /// This is the recommended way to connect to a workspace as it supports
91    /// XDG-compliant path resolution and environment variable configuration.
92    ///
93    /// # Examples
94    ///
95    /// ```no_run
96    /// # use agtrace_sdk::Client;
97    /// # #[tokio::main]
98    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
99    /// // Use default path
100    /// let client = Client::builder().connect().await?;
101    ///
102    /// // Use custom path
103    /// let client = Client::builder()
104    ///     .path("/custom/agtrace")
105    ///     .connect().await?;
106    /// # Ok(())
107    /// # }
108    /// ```
109    pub fn builder() -> ClientBuilder {
110        ClientBuilder::new()
111    }
112
113    /// Connect to the default agtrace workspace.
114    ///
115    /// This is a convenience method that uses XDG-compliant path resolution.
116    /// It checks (in order):
117    /// 1. `AGTRACE_PATH` environment variable
118    /// 2. System data directory + "agtrace" (e.g., `~/.local/share/agtrace`)
119    ///
120    /// # Examples
121    ///
122    /// ```no_run
123    /// # use agtrace_sdk::Client;
124    /// # #[tokio::main]
125    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
126    /// let client = Client::connect_default().await?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub async fn connect_default() -> Result<Self> {
131        Self::builder().connect().await
132    }
133
134    /// Connect to an agtrace workspace at the given path.
135    ///
136    /// This is a low-level API. Consider using `Client::builder()` or
137    /// `Client::connect_default()` for better ergonomics and XDG support.
138    ///
139    /// # Examples
140    ///
141    /// ```no_run
142    /// # use agtrace_sdk::Client;
143    /// # #[tokio::main]
144    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
145    /// let client = Client::connect("/custom/agtrace/path").await?;
146    /// # Ok(())
147    /// # }
148    /// ```
149    pub async fn connect(path: impl Into<PathBuf>) -> Result<Self> {
150        Self::builder().path(path).connect().await
151    }
152
153    /// Access session operations.
154    pub fn sessions(&self) -> SessionClient {
155        SessionClient {
156            inner: self.inner.clone(),
157        }
158    }
159
160    /// Access project operations.
161    pub fn projects(&self) -> ProjectClient {
162        ProjectClient {
163            inner: self.inner.clone(),
164        }
165    }
166
167    /// Access watch/monitoring operations.
168    pub fn watch(&self) -> WatchClient {
169        WatchClient {
170            inner: self.inner.clone(),
171        }
172    }
173
174    /// Access insights/analysis operations.
175    pub fn insights(&self) -> InsightClient {
176        InsightClient {
177            inner: self.inner.clone(),
178        }
179    }
180
181    /// Access system operations (init, index, doctor, provider).
182    pub fn system(&self) -> SystemClient {
183        SystemClient {
184            inner: self.inner.clone(),
185        }
186    }
187
188    /// Get the watch service for low-level watch operations.
189    /// Prefer using `client.watch()` for most use cases.
190    pub fn watch_service(&self) -> crate::types::WatchService {
191        self.inner.watch_service()
192    }
193}
194
195// ============================================================================
196// SessionClient
197// ============================================================================
198
199/// Client for session-related operations.
200pub struct SessionClient {
201    inner: Arc<agtrace_runtime::AgTrace>,
202}
203
204impl SessionClient {
205    /// List sessions with optional filtering.
206    pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
207        self.inner.sessions().list(filter).map_err(Error::Runtime)
208    }
209
210    /// List sessions without triggering auto-refresh.
211    pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
212        self.inner
213            .sessions()
214            .list_without_refresh(filter)
215            .map_err(Error::Runtime)
216    }
217
218    /// Pack sessions for context window analysis.
219    pub fn pack_context(
220        &self,
221        project_hash: Option<&crate::types::ProjectHash>,
222        limit: usize,
223    ) -> Result<crate::types::PackResult> {
224        self.inner
225            .sessions()
226            .pack_context(project_hash, limit)
227            .map_err(Error::Runtime)
228    }
229
230    /// Get a session handle by ID or prefix.
231    pub fn get(&self, id_or_prefix: &str) -> Result<SessionHandle> {
232        // Validate the session exists by trying to find it
233        self.inner
234            .sessions()
235            .find(id_or_prefix)
236            .map_err(|e| Error::NotFound(format!("Session {}: {}", id_or_prefix, e)))?;
237
238        Ok(SessionHandle {
239            source: SessionSource::Workspace {
240                inner: self.inner.clone(),
241                id: id_or_prefix.to_string(),
242            },
243        })
244    }
245}
246
247// ============================================================================
248// SessionHandle
249// ============================================================================
250
251/// Handle to a specific session, providing access to its data.
252pub struct SessionHandle {
253    source: SessionSource,
254}
255
256enum SessionSource {
257    /// Session from a workspace (Client-based)
258    Workspace {
259        inner: Arc<agtrace_runtime::AgTrace>,
260        id: String,
261    },
262    /// Session from raw events (Standalone)
263    Events {
264        events: Vec<crate::types::AgentEvent>,
265    },
266}
267
268impl SessionHandle {
269    /// Create a SessionHandle from raw events (for testing, simulations, custom pipelines).
270    ///
271    /// This allows you to use SessionHandle API without a Client connection.
272    ///
273    /// # Examples
274    ///
275    /// ```no_run
276    /// use agtrace_sdk::{SessionHandle, types::AgentEvent};
277    ///
278    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
279    /// let events: Vec<AgentEvent> = vec![/* ... */];
280    /// let handle = SessionHandle::from_events(events);
281    ///
282    /// let session = handle.assemble()?;
283    /// let summary = handle.summarize()?;
284    /// # Ok(())
285    /// # }
286    /// ```
287    pub fn from_events(events: Vec<AgentEvent>) -> Self {
288        Self {
289            source: SessionSource::Events { events },
290        }
291    }
292
293    /// Load raw events for this session.
294    pub fn events(&self) -> Result<Vec<AgentEvent>> {
295        match &self.source {
296            SessionSource::Workspace { inner, id } => {
297                let session_handle = inner
298                    .sessions()
299                    .find(id)
300                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
301
302                session_handle.events().map_err(Error::Runtime)
303            }
304            SessionSource::Events { events } => Ok(events.clone()),
305        }
306    }
307
308    /// Assemble events into a structured session.
309    pub fn assemble(&self) -> Result<AgentSession> {
310        let events = self.events()?;
311        agtrace_engine::assemble_session(&events).ok_or_else(|| {
312            Error::InvalidInput(
313                "Failed to assemble session: insufficient or invalid events".to_string(),
314            )
315        })
316    }
317
318    /// Export session with specified strategy.
319    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
320        let events = self.events()?;
321        Ok(agtrace_engine::export::transform(&events, strategy))
322    }
323
324    /// Summarize session statistics.
325    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
326        let session = self.assemble()?;
327        Ok(agtrace_engine::session::summarize(&session))
328    }
329
330    /// Analyze session with diagnostic lenses.
331    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
332        let session = self.assemble()?;
333        Ok(crate::analysis::SessionAnalyzer::new(session))
334    }
335}
336
337// ============================================================================
338// ProjectClient
339// ============================================================================
340
341/// Client for project-related operations.
342pub struct ProjectClient {
343    inner: Arc<agtrace_runtime::AgTrace>,
344}
345
346impl ProjectClient {
347    /// List all projects in the workspace.
348    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
349        self.inner.projects().list().map_err(Error::Runtime)
350    }
351}
352
353// ============================================================================
354// WatchClient
355// ============================================================================
356
357/// Client for live monitoring operations.
358pub struct WatchClient {
359    inner: Arc<agtrace_runtime::AgTrace>,
360}
361
362impl WatchClient {
363    /// Create a watch builder for configuring monitoring.
364    pub fn builder(&self) -> WatchBuilder {
365        WatchBuilder::new(self.inner.clone())
366    }
367
368    /// Watch all providers (convenience method).
369    pub fn all_providers(&self) -> WatchBuilder {
370        WatchBuilder::new(self.inner.clone()).all_providers()
371    }
372
373    /// Watch a specific provider (convenience method).
374    pub fn provider(&self, name: &str) -> WatchBuilder {
375        WatchBuilder::new(self.inner.clone()).provider(name)
376    }
377
378    /// Watch a specific session (convenience method).
379    pub fn session(&self, _id: &str) -> WatchBuilder {
380        // WatchBuilder doesn't have a session method yet, return builder for now
381        WatchBuilder::new(self.inner.clone())
382    }
383}
384
385// ============================================================================
386// InsightClient
387// ============================================================================
388
389/// Client for analysis and insights operations.
390pub struct InsightClient {
391    inner: Arc<agtrace_runtime::AgTrace>,
392}
393
394impl InsightClient {
395    /// Get corpus statistics.
396    pub fn corpus_stats(
397        &self,
398        project_hash: Option<&agtrace_types::ProjectHash>,
399        limit: usize,
400    ) -> Result<CorpusStats> {
401        self.inner
402            .insights()
403            .corpus_stats(project_hash, limit)
404            .map_err(Error::Runtime)
405    }
406
407    /// Get tool usage statistics.
408    pub fn tool_usage(
409        &self,
410        limit: Option<usize>,
411        provider: Option<String>,
412    ) -> Result<agtrace_runtime::StatsResult> {
413        self.inner
414            .insights()
415            .tool_usage(limit, provider)
416            .map_err(Error::Runtime)
417    }
418
419    /// Pack sessions for analysis (placeholder - needs runtime implementation).
420    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
421        // TODO: This needs to be implemented in agtrace-runtime
422        Err(Error::InvalidInput(
423            "Pack operation not yet implemented in runtime".to_string(),
424        ))
425    }
426
427    /// Grep through tool calls (placeholder - needs runtime implementation).
428    pub fn grep(
429        &self,
430        _pattern: &str,
431        _filter: &SessionFilter,
432        _limit: usize,
433    ) -> Result<Vec<AgentEvent>> {
434        // TODO: This needs to be implemented in agtrace-runtime
435        Err(Error::InvalidInput(
436            "Grep operation not yet implemented in runtime".to_string(),
437        ))
438    }
439}
440
441// ============================================================================
442// SystemClient
443// ============================================================================
444
445/// Client for system-level operations (init, index, doctor, provider).
446pub struct SystemClient {
447    inner: Arc<agtrace_runtime::AgTrace>,
448}
449
450impl SystemClient {
451    /// Initialize a new workspace (static method).
452    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
453    where
454        F: FnMut(InitProgress),
455    {
456        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
457    }
458
459    /// Run diagnostics on all providers.
460    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
461        self.inner.diagnose().map_err(Error::Runtime)
462    }
463
464    /// Check if a file can be parsed (requires workspace context).
465    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
466        let path_str = path
467            .to_str()
468            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
469
470        // Detect adapter
471        let (adapter, provider_name) = if let Some(name) = provider {
472            let adapter = agtrace_providers::create_adapter(name)
473                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
474            (adapter, name.to_string())
475        } else {
476            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
477                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
478            let name = format!("{} (auto-detected)", adapter.id());
479            (adapter, name)
480        };
481
482        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
483            .map_err(Error::Runtime)
484    }
485
486    /// Inspect file contents with parsing.
487    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
488        let path_str = path
489            .to_str()
490            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
491
492        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
493    }
494
495    /// Reindex the workspace.
496    pub fn reindex<F>(
497        &self,
498        scope: agtrace_types::ProjectScope,
499        force: bool,
500        provider_filter: Option<&str>,
501        on_progress: F,
502    ) -> Result<()>
503    where
504        F: FnMut(IndexProgress),
505    {
506        self.inner
507            .projects()
508            .scan(scope, force, provider_filter, on_progress)
509            .map(|_| ()) // Discard the ScanSummary for now
510            .map_err(Error::Runtime)
511    }
512
513    /// Vacuum the database to reclaim space.
514    pub fn vacuum(&self) -> Result<()> {
515        let db = self.inner.database();
516        let db = db.lock().unwrap();
517        db.vacuum().map_err(|e| Error::Runtime(e.into()))
518    }
519
520    /// List provider configurations.
521    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
522        Ok(self.inner.config().providers.values().cloned().collect())
523    }
524
525    /// Detect providers in current environment.
526    pub fn detect_providers() -> Result<Config> {
527        agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
528    }
529
530    /// Get current configuration.
531    pub fn config(&self) -> Config {
532        self.inner.config().clone()
533    }
534}