agtrace_sdk/
client.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::error::{Error, Result};
5use crate::providers::Providers;
6use crate::query::{
7    Cursor, EventMatch, GetTurnsArgs, GetTurnsResponse, ListTurnsArgs, ListTurnsResponse,
8    SearchEventsArgs, SearchEventsResponse,
9};
10use crate::types::*;
11use crate::watch::WatchBuilder;
12
13// ============================================================================
14// ClientBuilder
15// ============================================================================
16
17/// Builder for configuring and connecting to an agtrace workspace.
18///
19/// Provides flexible path resolution with the following priority:
20/// 1. Explicit path via `builder.path()`
21/// 2. `AGTRACE_PATH` environment variable
22/// 3. System data directory (e.g., `~/.local/share/agtrace` on Linux, `~/Library/Application Support/agtrace` on macOS)
23///
24/// # Examples
25///
26/// ```no_run
27/// # use agtrace_sdk::Client;
28/// # #[tokio::main]
29/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
30/// // Use default system path
31/// let client = Client::connect_default().await?;
32///
33/// // Use explicit path
34/// let client = Client::builder()
35///     .path("/custom/path")
36///     .connect().await?;
37///
38/// // Use AGTRACE_PATH environment variable
39/// // $ export AGTRACE_PATH=/tmp/agtrace
40/// let client = Client::builder().connect().await?;
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Default)]
45pub struct ClientBuilder {
46    path: Option<PathBuf>,
47}
48
49impl ClientBuilder {
50    /// Create a new ClientBuilder with default settings.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Set an explicit workspace path (highest priority).
56    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
57        self.path = Some(path.into());
58        self
59    }
60
61    /// Connect to the workspace using the configured or resolved path.
62    /// If the workspace does not exist, it will be automatically initialized.
63    pub async fn connect(self) -> Result<Client> {
64        let path = self.resolve_path()?;
65        let runtime = agtrace_runtime::AgTrace::connect_or_create(path)
66            .await
67            .map_err(Error::Runtime)?;
68        Ok(Client {
69            inner: Arc::new(runtime),
70        })
71    }
72
73    /// Resolve the workspace path based on priority:
74    /// 1. Explicit path from builder
75    /// 2. AGTRACE_PATH environment variable
76    /// 3. System data directory
77    fn resolve_path(&self) -> Result<PathBuf> {
78        let explicit_path = self.path.as_ref().and_then(|p| p.to_str());
79        agtrace_runtime::resolve_workspace_path(explicit_path).map_err(Error::Runtime)
80    }
81}
82
83// ============================================================================
84// Main Client
85// ============================================================================
86
87/// Main entry point for interacting with an agtrace workspace.
88#[derive(Clone)]
89pub struct Client {
90    inner: Arc<agtrace_runtime::AgTrace>,
91}
92
93impl Client {
94    /// Create a new ClientBuilder for configuring workspace connection.
95    ///
96    /// This is the recommended way to connect to a workspace as it supports
97    /// platform-standard path resolution and environment variable configuration.
98    ///
99    /// # Examples
100    ///
101    /// ```no_run
102    /// # use agtrace_sdk::Client;
103    /// # #[tokio::main]
104    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105    /// // Use default path
106    /// let client = Client::builder().connect().await?;
107    ///
108    /// // Use custom path
109    /// let client = Client::builder()
110    ///     .path("/custom/agtrace")
111    ///     .connect().await?;
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub fn builder() -> ClientBuilder {
116        ClientBuilder::new()
117    }
118
119    /// Connect to the default agtrace workspace.
120    ///
121    /// This is a convenience method that uses platform-standard path resolution.
122    /// It checks (in order):
123    /// 1. `AGTRACE_PATH` environment variable
124    /// 2. System data directory + "agtrace" (e.g., `~/.local/share/agtrace`)
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// # use agtrace_sdk::Client;
130    /// # #[tokio::main]
131    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
132    /// let client = Client::connect_default().await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn connect_default() -> Result<Self> {
137        Self::builder().connect().await
138    }
139
140    /// Connect to an agtrace workspace at the given path.
141    ///
142    /// This is a low-level API. Consider using `Client::builder()` or
143    /// `Client::connect_default()` for better ergonomics and system path support.
144    ///
145    /// # Examples
146    ///
147    /// ```no_run
148    /// # use agtrace_sdk::Client;
149    /// # #[tokio::main]
150    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
151    /// let client = Client::connect("/custom/agtrace/path").await?;
152    /// # Ok(())
153    /// # }
154    /// ```
155    pub async fn connect(path: impl Into<PathBuf>) -> Result<Self> {
156        Self::builder().path(path).connect().await
157    }
158
159    /// Access session operations.
160    pub fn sessions(&self) -> SessionClient {
161        SessionClient {
162            inner: self.inner.clone(),
163        }
164    }
165
166    /// Access project operations.
167    pub fn projects(&self) -> ProjectClient {
168        ProjectClient {
169            inner: self.inner.clone(),
170        }
171    }
172
173    /// Access watch/monitoring operations.
174    pub fn watch(&self) -> WatchClient {
175        WatchClient {
176            inner: self.inner.clone(),
177        }
178    }
179
180    /// Access insights/analysis operations.
181    pub fn insights(&self) -> InsightClient {
182        InsightClient {
183            inner: self.inner.clone(),
184        }
185    }
186
187    /// Access system operations (init, index, doctor, provider).
188    pub fn system(&self) -> SystemClient {
189        SystemClient {
190            inner: self.inner.clone(),
191        }
192    }
193
194    /// Get the watch service for low-level watch operations.
195    /// Prefer using `client.watch()` for most use cases.
196    pub fn watch_service(&self) -> crate::types::WatchService {
197        self.inner.watch_service()
198    }
199
200    /// Access lightweight provider operations.
201    ///
202    /// Returns a [`Providers`] instance configured with the workspace's providers.
203    /// Use this for operations that don't require database access:
204    /// - Parsing log files
205    /// - Running diagnostics
206    /// - Checking file parseability
207    ///
208    /// # Examples
209    ///
210    /// ```no_run
211    /// use agtrace_sdk::Client;
212    /// use std::path::Path;
213    ///
214    /// # #[tokio::main]
215    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216    /// let client = Client::connect_default().await?;
217    ///
218    /// // Parse a file using the workspace's provider configuration
219    /// let events = client.providers().parse_auto(Path::new("/path/to/log.jsonl"))?;
220    /// println!("Parsed {} events", events.len());
221    /// # Ok(())
222    /// # }
223    /// ```
224    pub fn providers(&self) -> Providers {
225        Providers::with_config(self.inner.config().clone())
226    }
227}
228
229// ============================================================================
230// SessionClient
231// ============================================================================
232
233/// Client for session-related operations.
234pub struct SessionClient {
235    inner: Arc<agtrace_runtime::AgTrace>,
236}
237
238impl SessionClient {
239    /// List sessions with optional filtering.
240    pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
241        self.inner.sessions().list(filter).map_err(Error::Runtime)
242    }
243
244    /// List sessions without triggering auto-refresh.
245    pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
246        self.inner
247            .sessions()
248            .list_without_refresh(filter)
249            .map_err(Error::Runtime)
250    }
251
252    /// Pack sessions for context window analysis.
253    pub fn pack_context(
254        &self,
255        project_hash: Option<&crate::types::ProjectHash>,
256        limit: usize,
257    ) -> Result<crate::types::PackResult> {
258        self.inner
259            .sessions()
260            .pack_context(project_hash, limit)
261            .map_err(Error::Runtime)
262    }
263
264    /// Get a session handle by ID or prefix.
265    pub fn get(&self, id_or_prefix: &str) -> Result<SessionHandle> {
266        // Validate the session exists by trying to find it
267        self.inner
268            .sessions()
269            .find(id_or_prefix)
270            .map_err(|e| Error::NotFound(format!("Session {}: {}", id_or_prefix, e)))?;
271
272        Ok(SessionHandle {
273            source: SessionSource::Workspace {
274                inner: self.inner.clone(),
275                id: id_or_prefix.to_string(),
276            },
277        })
278    }
279
280    // ========================================================================
281    // MCP Query Methods
282    // ========================================================================
283
284    /// Search events across sessions.
285    pub fn search_events(&self, args: SearchEventsArgs) -> Result<SearchEventsResponse> {
286        let limit = args.limit();
287        let offset = args
288            .cursor
289            .as_ref()
290            .and_then(|c| Cursor::decode(c))
291            .map(|c| c.offset)
292            .unwrap_or(0);
293
294        let project_hash_filter = if let Some(ref root) = args.project_root {
295            Some(crate::utils::project_hash_from_root(root))
296        } else {
297            args.project_hash.clone().map(|h| h.into())
298        };
299
300        let mut filter = if let Some(hash) = project_hash_filter {
301            SessionFilter::project(hash).limit(1000)
302        } else {
303            SessionFilter::all().limit(1000)
304        };
305
306        if let Some(ref provider) = args.provider {
307            filter = filter.provider(provider.as_str().to_string());
308        }
309
310        let sessions = if let Some(ref session_id) = args.session_id {
311            let _handle = self.get(session_id)?;
312
313            vec![SessionSummary {
314                id: session_id.clone(),
315                provider: String::new(),
316                project_hash: ProjectHash::from(String::new()),
317                project_root: None,
318                start_ts: None,
319                snippet: None,
320                parent_session_id: None,
321                spawned_by: None,
322            }]
323        } else {
324            self.list_without_refresh(filter)?
325        };
326
327        let mut all_matches = Vec::new();
328
329        for session_summary in sessions {
330            let handle = match self.get(&session_summary.id) {
331                Ok(h) => h,
332                Err(_) => continue,
333            };
334
335            let session = match handle.assemble() {
336                Ok(s) => s,
337                Err(_) => continue,
338            };
339
340            let events = match handle.events() {
341                Ok(e) => e,
342                Err(_) => continue,
343            };
344
345            for (event_index, event) in events.iter().enumerate() {
346                if let Some(ref event_type_filter) = args.event_type
347                    && !event_type_filter.matches_payload(&event.payload)
348                {
349                    continue;
350                }
351
352                let event_json = match serde_json::to_string(&event.payload) {
353                    Ok(j) => j,
354                    Err(_) => continue,
355                };
356
357                if event_json.contains(&args.query) {
358                    let (turn_index, step_index) = Self::find_event_location(&session, event_index);
359
360                    let event_match = EventMatch::new(
361                        session_summary.id.clone(),
362                        event_index,
363                        turn_index,
364                        step_index,
365                        event,
366                    );
367                    all_matches.push(event_match);
368                }
369            }
370        }
371
372        let fetch_limit = limit + 1;
373        let mut matches: Vec<_> = all_matches
374            .into_iter()
375            .skip(offset)
376            .take(fetch_limit)
377            .collect();
378
379        let has_more = matches.len() > limit;
380        if has_more {
381            matches.pop();
382        }
383
384        let next_cursor = if has_more {
385            Some(
386                Cursor {
387                    offset: offset + limit,
388                }
389                .encode(),
390            )
391        } else {
392            None
393        };
394
395        Ok(SearchEventsResponse {
396            matches,
397            next_cursor,
398        })
399    }
400
401    /// List turns with metadata (no payload).
402    pub fn list_turns(&self, args: ListTurnsArgs) -> Result<ListTurnsResponse> {
403        let handle = self.get(&args.session_id)?;
404
405        let session = handle.assemble()?;
406
407        let limit = args.limit();
408        let offset = args
409            .cursor
410            .as_ref()
411            .and_then(|c| Cursor::decode(c))
412            .map(|c| c.offset)
413            .unwrap_or(0);
414
415        let total_turns = session.turns.len();
416        let remaining = total_turns.saturating_sub(offset);
417        let has_more = remaining > limit;
418
419        let next_cursor = if has_more {
420            Some(
421                Cursor {
422                    offset: offset + limit,
423                }
424                .encode(),
425            )
426        } else {
427            None
428        };
429
430        Ok(ListTurnsResponse::new(session, offset, limit, next_cursor))
431    }
432
433    /// Get specific turns with safety valves.
434    pub fn get_turns(&self, args: GetTurnsArgs) -> Result<GetTurnsResponse> {
435        let handle = self.get(&args.session_id)?;
436
437        let session = handle.assemble()?;
438
439        GetTurnsResponse::new(session, &args).map_err(Error::InvalidInput)
440    }
441
442    fn find_event_location(session: &AgentSession, event_index: usize) -> (usize, usize) {
443        let mut current_event_idx = 0;
444
445        for (turn_idx, turn) in session.turns.iter().enumerate() {
446            for (step_idx, step) in turn.steps.iter().enumerate() {
447                let step_event_count = Self::count_step_events(step);
448
449                if current_event_idx + step_event_count > event_index {
450                    return (turn_idx, step_idx);
451                }
452
453                current_event_idx += step_event_count;
454            }
455        }
456
457        (0, 0)
458    }
459
460    fn count_step_events(step: &AgentStep) -> usize {
461        let mut count = 0;
462
463        if step.reasoning.is_some() {
464            count += 1;
465        }
466
467        count += step.tools.len() * 2;
468
469        if step.message.is_some() {
470            count += 1;
471        }
472
473        count
474    }
475}
476
477// ============================================================================
478// SessionHandle
479// ============================================================================
480
481/// Handle to a specific session, providing access to its data.
482pub struct SessionHandle {
483    source: SessionSource,
484}
485
486enum SessionSource {
487    /// Session from a workspace (Client-based)
488    Workspace {
489        inner: Arc<agtrace_runtime::AgTrace>,
490        id: String,
491    },
492    /// Session from raw events (Standalone)
493    Events {
494        events: Vec<crate::types::AgentEvent>,
495    },
496}
497
498impl SessionHandle {
499    /// Create a SessionHandle from raw events (for testing, simulations, custom pipelines).
500    ///
501    /// This allows you to use SessionHandle API without a Client connection.
502    ///
503    /// # Examples
504    ///
505    /// ```no_run
506    /// use agtrace_sdk::{SessionHandle, types::AgentEvent};
507    ///
508    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
509    /// let events: Vec<AgentEvent> = vec![/* ... */];
510    /// let handle = SessionHandle::from_events(events);
511    ///
512    /// let session = handle.assemble()?;
513    /// let summary = handle.summarize()?;
514    /// # Ok(())
515    /// # }
516    /// ```
517    pub fn from_events(events: Vec<AgentEvent>) -> Self {
518        Self {
519            source: SessionSource::Events { events },
520        }
521    }
522
523    /// Load raw events for this session.
524    pub fn events(&self) -> Result<Vec<AgentEvent>> {
525        match &self.source {
526            SessionSource::Workspace { inner, id } => {
527                let session_handle = inner
528                    .sessions()
529                    .find(id)
530                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
531
532                session_handle.events().map_err(Error::Runtime)
533            }
534            SessionSource::Events { events } => Ok(events.clone()),
535        }
536    }
537
538    /// Assemble events into a structured session.
539    ///
540    /// Returns only the main stream. For multi-stream sessions (with sidechains
541    /// or subagents), use `assemble_all()` instead.
542    pub fn assemble(&self) -> Result<AgentSession> {
543        let events = self.events()?;
544        agtrace_engine::assemble_session(&events).ok_or_else(|| {
545            Error::InvalidInput(
546                "Failed to assemble session: insufficient or invalid events".to_string(),
547            )
548        })
549    }
550
551    /// Assemble all streams from events into separate sessions.
552    ///
553    /// Unlike `assemble()` which returns only the main stream, this method
554    /// returns all streams (Main, Sidechain, Subagent) found in the session's events.
555    pub fn assemble_all(&self) -> Result<Vec<AgentSession>> {
556        let events = self.events()?;
557        let sessions = agtrace_engine::assemble_sessions(&events);
558        if sessions.is_empty() {
559            return Err(Error::InvalidInput(
560                "Failed to assemble session: insufficient or invalid events".to_string(),
561            ));
562        }
563        Ok(sessions)
564    }
565
566    /// Export session with specified strategy.
567    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
568        let events = self.events()?;
569        Ok(agtrace_engine::export::transform(&events, strategy))
570    }
571
572    /// Get session metadata (DB-derived: project_hash, provider).
573    ///
574    /// Returns None for standalone sessions (created from events without workspace).
575    pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
576        match &self.source {
577            SessionSource::Workspace { inner, id } => {
578                let runtime_handle = inner
579                    .sessions()
580                    .find(id)
581                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
582
583                runtime_handle.metadata().map(Some).map_err(Error::Runtime)
584            }
585            SessionSource::Events { .. } => Ok(None),
586        }
587    }
588
589    /// Get raw log files for this session.
590    ///
591    /// Returns the list of raw log file paths and their contents.
592    /// Returns empty vector for standalone sessions (created from events without workspace).
593    pub fn raw_files(&self) -> Result<Vec<crate::types::RawFileContent>> {
594        match &self.source {
595            SessionSource::Workspace { inner, id } => {
596                let runtime_handle = inner
597                    .sessions()
598                    .find(id)
599                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
600
601                runtime_handle.raw_files().map_err(Error::Runtime)
602            }
603            SessionSource::Events { .. } => Ok(vec![]),
604        }
605    }
606
607    /// Summarize session statistics.
608    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
609        let session = self.assemble()?;
610        Ok(agtrace_engine::session::summarize(&session))
611    }
612
613    /// Analyze session with diagnostic lenses.
614    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
615        let session = self.assemble()?;
616        Ok(crate::analysis::SessionAnalyzer::new(session))
617    }
618
619    /// Get child sessions (subagents) that were spawned from this session.
620    ///
621    /// Returns a list of child session summaries with their spawn context
622    /// (turn_index, step_index). Returns empty vector for standalone sessions.
623    pub fn child_sessions(&self) -> Result<Vec<ChildSessionInfo>> {
624        match &self.source {
625            SessionSource::Workspace { inner, id } => {
626                let runtime_handle = inner
627                    .sessions()
628                    .find(id)
629                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
630
631                let children = runtime_handle.child_sessions().map_err(Error::Runtime)?;
632                Ok(children
633                    .into_iter()
634                    .map(|c| ChildSessionInfo {
635                        session_id: c.id,
636                        provider: c.provider,
637                        spawned_by: c.spawned_by,
638                        snippet: c.snippet,
639                    })
640                    .collect())
641            }
642            SessionSource::Events { .. } => Ok(vec![]),
643        }
644    }
645}
646
647/// Information about a child session (subagent) spawned from a parent session.
648#[derive(Debug, Clone)]
649pub struct ChildSessionInfo {
650    pub session_id: String,
651    pub provider: String,
652    pub spawned_by: Option<agtrace_types::SpawnContext>,
653    pub snippet: Option<String>,
654}
655
656// ============================================================================
657// ProjectClient
658// ============================================================================
659
660/// Client for project-related operations.
661pub struct ProjectClient {
662    inner: Arc<agtrace_runtime::AgTrace>,
663}
664
665impl ProjectClient {
666    /// List all projects in the workspace.
667    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
668        self.inner.projects().list().map_err(Error::Runtime)
669    }
670}
671
672// ============================================================================
673// WatchClient
674// ============================================================================
675
676/// Client for live monitoring operations.
677pub struct WatchClient {
678    inner: Arc<agtrace_runtime::AgTrace>,
679}
680
681impl WatchClient {
682    /// Create a watch builder for configuring monitoring.
683    pub fn builder(&self) -> WatchBuilder {
684        WatchBuilder::new(self.inner.clone())
685    }
686
687    /// Watch all providers (convenience method).
688    pub fn all_providers(&self) -> WatchBuilder {
689        WatchBuilder::new(self.inner.clone()).all_providers()
690    }
691
692    /// Watch a specific provider (convenience method).
693    pub fn provider(&self, name: &str) -> WatchBuilder {
694        WatchBuilder::new(self.inner.clone()).provider(name)
695    }
696
697    /// Watch a specific session (convenience method).
698    pub fn session(&self, _id: &str) -> WatchBuilder {
699        // WatchBuilder doesn't have a session method yet, return builder for now
700        WatchBuilder::new(self.inner.clone())
701    }
702}
703
704// ============================================================================
705// InsightClient
706// ============================================================================
707
708/// Client for analysis and insights operations.
709pub struct InsightClient {
710    inner: Arc<agtrace_runtime::AgTrace>,
711}
712
713impl InsightClient {
714    /// Get corpus statistics.
715    pub fn corpus_stats(
716        &self,
717        project_hash: Option<&agtrace_types::ProjectHash>,
718        limit: usize,
719    ) -> Result<CorpusStats> {
720        self.inner
721            .insights()
722            .corpus_stats(project_hash, limit)
723            .map_err(Error::Runtime)
724    }
725
726    /// Get tool usage statistics.
727    pub fn tool_usage(
728        &self,
729        limit: Option<usize>,
730        provider: Option<String>,
731    ) -> Result<agtrace_runtime::StatsResult> {
732        self.inner
733            .insights()
734            .tool_usage(limit, provider)
735            .map_err(Error::Runtime)
736    }
737
738    /// Pack sessions for analysis (placeholder - needs runtime implementation).
739    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
740        // TODO: This needs to be implemented in agtrace-runtime
741        Err(Error::InvalidInput(
742            "Pack operation not yet implemented in runtime".to_string(),
743        ))
744    }
745
746    /// Grep through tool calls (placeholder - needs runtime implementation).
747    pub fn grep(
748        &self,
749        _pattern: &str,
750        _filter: &SessionFilter,
751        _limit: usize,
752    ) -> Result<Vec<AgentEvent>> {
753        // TODO: This needs to be implemented in agtrace-runtime
754        Err(Error::InvalidInput(
755            "Grep operation not yet implemented in runtime".to_string(),
756        ))
757    }
758}
759
760// ============================================================================
761// SystemClient
762// ============================================================================
763
764/// Client for system-level operations (init, index, doctor, provider).
765pub struct SystemClient {
766    inner: Arc<agtrace_runtime::AgTrace>,
767}
768
769impl SystemClient {
770    /// Initialize a new workspace (static method).
771    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
772    where
773        F: FnMut(InitProgress),
774    {
775        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
776    }
777
778    /// Run diagnostics on all providers.
779    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
780        self.inner.diagnose().map_err(Error::Runtime)
781    }
782
783    /// Check if a file can be parsed (requires workspace context).
784    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
785        let path_str = path
786            .to_str()
787            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
788
789        // Detect adapter
790        let (adapter, provider_name) = if let Some(name) = provider {
791            let adapter = agtrace_providers::create_adapter(name)
792                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
793            (adapter, name.to_string())
794        } else {
795            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
796                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
797            let name = format!("{} (auto-detected)", adapter.id());
798            (adapter, name)
799        };
800
801        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
802            .map_err(Error::Runtime)
803    }
804
805    /// Inspect file contents with parsing.
806    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
807        let path_str = path
808            .to_str()
809            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
810
811        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
812    }
813
814    /// Reindex the workspace.
815    pub fn reindex<F>(
816        &self,
817        scope: agtrace_types::ProjectScope,
818        force: bool,
819        provider_filter: Option<&str>,
820        on_progress: F,
821    ) -> Result<()>
822    where
823        F: FnMut(IndexProgress),
824    {
825        self.inner
826            .projects()
827            .scan(scope, force, provider_filter, on_progress)
828            .map(|_| ()) // Discard the ScanSummary for now
829            .map_err(Error::Runtime)
830    }
831
832    /// Vacuum the database to reclaim space.
833    pub fn vacuum(&self) -> Result<()> {
834        let db = self.inner.database();
835        let db = db.lock().unwrap();
836        db.vacuum().map_err(|e| Error::Runtime(e.into()))
837    }
838
839    /// List provider configurations.
840    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
841        Ok(self.inner.config().providers.values().cloned().collect())
842    }
843
844    /// Detect providers in current environment.
845    pub fn detect_providers() -> Result<Config> {
846        agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
847    }
848
849    /// Get current configuration.
850    pub fn config(&self) -> Config {
851        self.inner.config().clone()
852    }
853}