Skip to main content

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                repository_hash: None,
318                project_root: None,
319                start_ts: None,
320                snippet: None,
321                parent_session_id: None,
322                spawned_by: None,
323            }]
324        } else {
325            self.list_without_refresh(filter)?
326        };
327
328        let mut all_matches = Vec::new();
329
330        for session_summary in sessions {
331            let handle = match self.get(&session_summary.id) {
332                Ok(h) => h,
333                Err(_) => continue,
334            };
335
336            let session = match handle.assemble() {
337                Ok(s) => s,
338                Err(_) => continue,
339            };
340
341            let events = match handle.events() {
342                Ok(e) => e,
343                Err(_) => continue,
344            };
345
346            for (event_index, event) in events.iter().enumerate() {
347                if let Some(ref event_type_filter) = args.event_type
348                    && !event_type_filter.matches_payload(&event.payload)
349                {
350                    continue;
351                }
352
353                let event_json = match serde_json::to_string(&event.payload) {
354                    Ok(j) => j,
355                    Err(_) => continue,
356                };
357
358                if event_json.contains(&args.query) {
359                    let (turn_index, step_index) = Self::find_event_location(&session, event_index);
360
361                    let event_match = EventMatch::new(
362                        session_summary.id.clone(),
363                        event_index,
364                        turn_index,
365                        step_index,
366                        event,
367                    );
368                    all_matches.push(event_match);
369                }
370            }
371        }
372
373        let fetch_limit = limit + 1;
374        let mut matches: Vec<_> = all_matches
375            .into_iter()
376            .skip(offset)
377            .take(fetch_limit)
378            .collect();
379
380        let has_more = matches.len() > limit;
381        if has_more {
382            matches.pop();
383        }
384
385        let next_cursor = if has_more {
386            Some(
387                Cursor {
388                    offset: offset + limit,
389                }
390                .encode(),
391            )
392        } else {
393            None
394        };
395
396        Ok(SearchEventsResponse {
397            matches,
398            next_cursor,
399        })
400    }
401
402    /// List turns with metadata (no payload).
403    pub fn list_turns(&self, args: ListTurnsArgs) -> Result<ListTurnsResponse> {
404        let handle = self.get(&args.session_id)?;
405
406        let session = handle.assemble()?;
407
408        let limit = args.limit();
409        let offset = args
410            .cursor
411            .as_ref()
412            .and_then(|c| Cursor::decode(c))
413            .map(|c| c.offset)
414            .unwrap_or(0);
415
416        let total_turns = session.turns.len();
417        let remaining = total_turns.saturating_sub(offset);
418        let has_more = remaining > limit;
419
420        let next_cursor = if has_more {
421            Some(
422                Cursor {
423                    offset: offset + limit,
424                }
425                .encode(),
426            )
427        } else {
428            None
429        };
430
431        Ok(ListTurnsResponse::new(session, offset, limit, next_cursor))
432    }
433
434    /// Get specific turns with safety valves.
435    pub fn get_turns(&self, args: GetTurnsArgs) -> Result<GetTurnsResponse> {
436        let handle = self.get(&args.session_id)?;
437
438        let session = handle.assemble()?;
439
440        GetTurnsResponse::new(session, &args).map_err(Error::InvalidInput)
441    }
442
443    fn find_event_location(session: &AgentSession, event_index: usize) -> (usize, usize) {
444        let mut current_event_idx = 0;
445
446        for (turn_idx, turn) in session.turns.iter().enumerate() {
447            for (step_idx, step) in turn.steps.iter().enumerate() {
448                let step_event_count = Self::count_step_events(step);
449
450                if current_event_idx + step_event_count > event_index {
451                    return (turn_idx, step_idx);
452                }
453
454                current_event_idx += step_event_count;
455            }
456        }
457
458        (0, 0)
459    }
460
461    fn count_step_events(step: &AgentStep) -> usize {
462        let mut count = 0;
463
464        if step.reasoning.is_some() {
465            count += 1;
466        }
467
468        count += step.tools.len() * 2;
469
470        if step.message.is_some() {
471            count += 1;
472        }
473
474        count
475    }
476}
477
478// ============================================================================
479// SessionHandle
480// ============================================================================
481
482/// Handle to a specific session, providing access to its data.
483pub struct SessionHandle {
484    source: SessionSource,
485}
486
487enum SessionSource {
488    /// Session from a workspace (Client-based)
489    Workspace {
490        inner: Arc<agtrace_runtime::AgTrace>,
491        id: String,
492    },
493    /// Session from raw events (Standalone)
494    Events {
495        events: Vec<crate::types::AgentEvent>,
496    },
497}
498
499impl SessionHandle {
500    /// Create a SessionHandle from raw events (for testing, simulations, custom pipelines).
501    ///
502    /// This allows you to use SessionHandle API without a Client connection.
503    ///
504    /// # Examples
505    ///
506    /// ```no_run
507    /// use agtrace_sdk::{SessionHandle, types::AgentEvent};
508    ///
509    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
510    /// let events: Vec<AgentEvent> = vec![/* ... */];
511    /// let handle = SessionHandle::from_events(events);
512    ///
513    /// let session = handle.assemble()?;
514    /// let summary = handle.summarize()?;
515    /// # Ok(())
516    /// # }
517    /// ```
518    pub fn from_events(events: Vec<AgentEvent>) -> Self {
519        Self {
520            source: SessionSource::Events { events },
521        }
522    }
523
524    /// Load raw events for this session.
525    pub fn events(&self) -> Result<Vec<AgentEvent>> {
526        match &self.source {
527            SessionSource::Workspace { inner, id } => {
528                let session_handle = inner
529                    .sessions()
530                    .find(id)
531                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
532
533                session_handle.events().map_err(Error::Runtime)
534            }
535            SessionSource::Events { events } => Ok(events.clone()),
536        }
537    }
538
539    /// Assemble events into a structured session.
540    ///
541    /// Returns only the main stream. For multi-stream sessions (with sidechains
542    /// or subagents), use `assemble_all()` instead.
543    pub fn assemble(&self) -> Result<AgentSession> {
544        let events = self.events()?;
545        agtrace_engine::assemble_session(&events).ok_or_else(|| {
546            Error::InvalidInput(
547                "Failed to assemble session: insufficient or invalid events".to_string(),
548            )
549        })
550    }
551
552    /// Assemble all streams from events into separate sessions.
553    ///
554    /// Unlike `assemble()` which returns only the main stream, this method
555    /// returns all streams (Main, Sidechain, Subagent) found in the session's events.
556    pub fn assemble_all(&self) -> Result<Vec<AgentSession>> {
557        let events = self.events()?;
558        let sessions = agtrace_engine::assemble_sessions(&events);
559        if sessions.is_empty() {
560            return Err(Error::InvalidInput(
561                "Failed to assemble session: insufficient or invalid events".to_string(),
562            ));
563        }
564        Ok(sessions)
565    }
566
567    /// Export session with specified strategy.
568    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
569        let events = self.events()?;
570        Ok(agtrace_engine::export::transform(&events, strategy))
571    }
572
573    /// Get session metadata (DB-derived: project_hash, provider).
574    ///
575    /// Returns None for standalone sessions (created from events without workspace).
576    pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
577        match &self.source {
578            SessionSource::Workspace { inner, id } => {
579                let runtime_handle = inner
580                    .sessions()
581                    .find(id)
582                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
583
584                runtime_handle.metadata().map(Some).map_err(Error::Runtime)
585            }
586            SessionSource::Events { .. } => Ok(None),
587        }
588    }
589
590    /// Get raw log files for this session.
591    ///
592    /// Returns the list of raw log file paths and their contents.
593    /// Returns empty vector for standalone sessions (created from events without workspace).
594    pub fn raw_files(&self) -> Result<Vec<crate::types::RawFileContent>> {
595        match &self.source {
596            SessionSource::Workspace { inner, id } => {
597                let runtime_handle = inner
598                    .sessions()
599                    .find(id)
600                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
601
602                runtime_handle.raw_files().map_err(Error::Runtime)
603            }
604            SessionSource::Events { .. } => Ok(vec![]),
605        }
606    }
607
608    /// Summarize session statistics.
609    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
610        let session = self.assemble()?;
611        Ok(agtrace_engine::session::summarize(&session))
612    }
613
614    /// Analyze session with diagnostic lenses.
615    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
616        let session = self.assemble()?;
617        Ok(crate::analysis::SessionAnalyzer::new(session))
618    }
619
620    /// Get child sessions (subagents) that were spawned from this session.
621    ///
622    /// Returns a list of child session summaries with their spawn context
623    /// (turn_index, step_index). Returns empty vector for standalone sessions.
624    pub fn child_sessions(&self) -> Result<Vec<ChildSessionInfo>> {
625        match &self.source {
626            SessionSource::Workspace { inner, id } => {
627                let runtime_handle = inner
628                    .sessions()
629                    .find(id)
630                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
631
632                let children = runtime_handle.child_sessions().map_err(Error::Runtime)?;
633                Ok(children
634                    .into_iter()
635                    .map(|c| ChildSessionInfo {
636                        session_id: c.id,
637                        provider: c.provider,
638                        spawned_by: c.spawned_by,
639                        snippet: c.snippet,
640                    })
641                    .collect())
642            }
643            SessionSource::Events { .. } => Ok(vec![]),
644        }
645    }
646}
647
648/// Information about a child session (subagent) spawned from a parent session.
649#[derive(Debug, Clone)]
650pub struct ChildSessionInfo {
651    pub session_id: String,
652    pub provider: String,
653    pub spawned_by: Option<agtrace_types::SpawnContext>,
654    pub snippet: Option<String>,
655}
656
657// ============================================================================
658// ProjectClient
659// ============================================================================
660
661/// Client for project-related operations.
662pub struct ProjectClient {
663    inner: Arc<agtrace_runtime::AgTrace>,
664}
665
666impl ProjectClient {
667    /// List all projects in the workspace.
668    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
669        self.inner.projects().list().map_err(Error::Runtime)
670    }
671}
672
673// ============================================================================
674// WatchClient
675// ============================================================================
676
677/// Client for live monitoring operations.
678pub struct WatchClient {
679    inner: Arc<agtrace_runtime::AgTrace>,
680}
681
682impl WatchClient {
683    /// Create a watch builder for configuring monitoring.
684    pub fn builder(&self) -> WatchBuilder {
685        WatchBuilder::new(self.inner.clone())
686    }
687
688    /// Watch all providers (convenience method).
689    pub fn all_providers(&self) -> WatchBuilder {
690        WatchBuilder::new(self.inner.clone()).all_providers()
691    }
692
693    /// Watch a specific provider (convenience method).
694    pub fn provider(&self, name: &str) -> WatchBuilder {
695        WatchBuilder::new(self.inner.clone()).provider(name)
696    }
697
698    /// Watch a specific session (convenience method).
699    pub fn session(&self, _id: &str) -> WatchBuilder {
700        // WatchBuilder doesn't have a session method yet, return builder for now
701        WatchBuilder::new(self.inner.clone())
702    }
703}
704
705// ============================================================================
706// InsightClient
707// ============================================================================
708
709/// Client for analysis and insights operations.
710pub struct InsightClient {
711    inner: Arc<agtrace_runtime::AgTrace>,
712}
713
714impl InsightClient {
715    /// Get corpus statistics.
716    pub fn corpus_stats(
717        &self,
718        project_hash: Option<&agtrace_types::ProjectHash>,
719        limit: usize,
720    ) -> Result<CorpusStats> {
721        self.inner
722            .insights()
723            .corpus_stats(project_hash, limit)
724            .map_err(Error::Runtime)
725    }
726
727    /// Get tool usage statistics.
728    pub fn tool_usage(
729        &self,
730        limit: Option<usize>,
731        provider: Option<String>,
732    ) -> Result<agtrace_runtime::StatsResult> {
733        self.inner
734            .insights()
735            .tool_usage(limit, provider)
736            .map_err(Error::Runtime)
737    }
738
739    /// Pack sessions for analysis (placeholder - needs runtime implementation).
740    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
741        // TODO: This needs to be implemented in agtrace-runtime
742        Err(Error::InvalidInput(
743            "Pack operation not yet implemented in runtime".to_string(),
744        ))
745    }
746
747    /// Grep through tool calls (placeholder - needs runtime implementation).
748    pub fn grep(
749        &self,
750        _pattern: &str,
751        _filter: &SessionFilter,
752        _limit: usize,
753    ) -> Result<Vec<AgentEvent>> {
754        // TODO: This needs to be implemented in agtrace-runtime
755        Err(Error::InvalidInput(
756            "Grep operation not yet implemented in runtime".to_string(),
757        ))
758    }
759}
760
761// ============================================================================
762// SystemClient
763// ============================================================================
764
765/// Client for system-level operations (init, index, doctor, provider).
766pub struct SystemClient {
767    inner: Arc<agtrace_runtime::AgTrace>,
768}
769
770impl SystemClient {
771    /// Initialize a new workspace (static method).
772    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
773    where
774        F: FnMut(InitProgress),
775    {
776        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
777    }
778
779    /// Run diagnostics on all providers.
780    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
781        self.inner.diagnose().map_err(Error::Runtime)
782    }
783
784    /// Check if a file can be parsed (requires workspace context).
785    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
786        let path_str = path
787            .to_str()
788            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
789
790        // Detect adapter
791        let (adapter, provider_name) = if let Some(name) = provider {
792            let adapter = agtrace_providers::create_adapter(name)
793                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
794            (adapter, name.to_string())
795        } else {
796            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
797                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
798            let name = format!("{} (auto-detected)", adapter.id());
799            (adapter, name)
800        };
801
802        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
803            .map_err(Error::Runtime)
804    }
805
806    /// Inspect file contents with parsing.
807    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
808        let path_str = path
809            .to_str()
810            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
811
812        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
813    }
814
815    /// Reindex the workspace.
816    pub fn reindex<F>(
817        &self,
818        scope: agtrace_types::ProjectScope,
819        force: bool,
820        provider_filter: Option<&str>,
821        on_progress: F,
822    ) -> Result<()>
823    where
824        F: FnMut(IndexProgress),
825    {
826        self.inner
827            .projects()
828            .scan(scope, force, provider_filter, on_progress)
829            .map(|_| ()) // Discard the ScanSummary for now
830            .map_err(Error::Runtime)
831    }
832
833    /// Vacuum the database to reclaim space.
834    pub fn vacuum(&self) -> Result<()> {
835        let db = self.inner.database();
836        let db = db.lock().unwrap();
837        db.vacuum().map_err(|e| Error::Runtime(e.into()))
838    }
839
840    /// List provider configurations.
841    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
842        Ok(self.inner.config().providers.values().cloned().collect())
843    }
844
845    /// Detect providers in current environment.
846    pub fn detect_providers() -> Result<Config> {
847        agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
848    }
849
850    /// Get current configuration.
851    pub fn config(&self) -> Config {
852        self.inner.config().clone()
853    }
854}