Skip to main content

agtrace_sdk/
providers.rs

1//! Lightweight provider operations without database.
2//!
3//! The [`Providers`] type provides access to provider operations (parsing, diagnostics)
4//! without requiring a full workspace with database. Use this when you only need to:
5//!
6//! - Parse log files directly
7//! - Run diagnostics on providers
8//! - Check file parseability
9//! - Inspect file contents
10//!
11//! For session querying and indexing, use [`Client`](crate::Client) instead.
12//!
13//! # Examples
14//!
15//! ```no_run
16//! use agtrace_sdk::Providers;
17//! use std::path::Path;
18//!
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! // Auto-detect providers from system paths
21//! let providers = Providers::detect()?;
22//!
23//! // Parse a log file
24//! let events = providers.parse_auto(Path::new("/path/to/log.jsonl"))?;
25//! println!("Parsed {} events", events.len());
26//!
27//! // Run diagnostics
28//! let results = providers.diagnose()?;
29//! for result in &results {
30//!     println!("{}: {} files, {} successful",
31//!         result.provider_name,
32//!         result.total_files,
33//!         result.successful);
34//! }
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ## Custom Provider Configuration
40//!
41//! ```no_run
42//! use agtrace_sdk::Providers;
43//!
44//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! let providers = Providers::builder()
46//!     .provider("claude_code", "/custom/.claude/projects")
47//!     .provider("codex", "/custom/.codex/sessions")
48//!     .build()?;
49//! # Ok(())
50//! # }
51//! ```
52
53use std::path::{Path, PathBuf};
54use std::sync::Arc;
55
56use crate::error::{Error, Result};
57use crate::types::{
58    AgentEvent, CheckResult, Config, DiagnoseResult, InspectResult, ProviderConfig,
59};
60
61/// Lightweight client for provider operations without database.
62///
63/// This type provides access to provider-level operations (parsing, diagnostics)
64/// without requiring a full workspace with database indexing.
65///
66/// # When to use `Providers` vs `Client`
67///
68/// | Operation | `Providers` | `Client` |
69/// |-----------|-------------|----------|
70/// | Parse log files | Yes | Yes (via `.providers()`) |
71/// | Run diagnostics | Yes | Yes (via `.system()`) |
72/// | Check/inspect files | Yes | Yes (via `.system()`) |
73/// | List sessions | No | Yes |
74/// | Query sessions | No | Yes |
75/// | Watch events | No | Yes |
76/// | Index operations | No | Yes |
77///
78/// Use `Providers` for:
79/// - Quick file parsing without workspace setup
80/// - Diagnostics on provider log directories
81/// - CI/CD validation of log files
82/// - Tools that only need read-only file access
83///
84/// Use `Client` for:
85/// - Session browsing and querying
86/// - Real-time event monitoring
87/// - Full workspace operations
88#[derive(Clone)]
89pub struct Providers {
90    config: Arc<Config>,
91    /// (provider_name, log_root)
92    provider_configs: Vec<(String, PathBuf)>,
93}
94
95impl Providers {
96    /// Create with auto-detected providers from system paths.
97    ///
98    /// Scans default log directories for each supported provider
99    /// (Claude Code, Codex, Gemini) and enables those that exist.
100    ///
101    /// # Examples
102    ///
103    /// ```no_run
104    /// use agtrace_sdk::Providers;
105    ///
106    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107    /// let providers = Providers::detect()?;
108    /// println!("Detected {} providers", providers.list().len());
109    /// # Ok(())
110    /// # }
111    /// ```
112    pub fn detect() -> Result<Self> {
113        let config = Config::detect_providers().map_err(Error::Runtime)?;
114        Ok(Self::with_config(config))
115    }
116
117    /// Create with explicit configuration.
118    ///
119    /// # Examples
120    ///
121    /// ```no_run
122    /// use agtrace_sdk::{Providers, types::Config};
123    ///
124    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
125    /// let config = Config::detect_providers()?;
126    /// let providers = Providers::with_config(config);
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub fn with_config(config: Config) -> Self {
131        let provider_configs: Vec<(String, PathBuf)> = config
132            .enabled_providers()
133            .into_iter()
134            .map(|(name, cfg)| (name.clone(), cfg.log_root.clone()))
135            .collect();
136
137        Self {
138            config: Arc::new(config),
139            provider_configs,
140        }
141    }
142
143    /// Create a builder for fine-grained configuration.
144    ///
145    /// # Examples
146    ///
147    /// ```no_run
148    /// use agtrace_sdk::Providers;
149    ///
150    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
151    /// let providers = Providers::builder()
152    ///     .provider("claude_code", "/custom/.claude/projects")
153    ///     .build()?;
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub fn builder() -> ProvidersBuilder {
158        ProvidersBuilder::new()
159    }
160
161    // =========================================================================
162    // Operations
163    // =========================================================================
164
165    /// Parse a log file with auto-detected provider.
166    ///
167    /// Automatically detects the appropriate provider based on file path
168    /// and parses it into events.
169    ///
170    /// # Examples
171    ///
172    /// ```no_run
173    /// use agtrace_sdk::Providers;
174    /// use std::path::Path;
175    ///
176    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
177    /// let providers = Providers::detect()?;
178    /// let events = providers.parse_auto(Path::new("/path/to/session.jsonl"))?;
179    /// for event in &events {
180    ///     println!("{:?}", event.payload);
181    /// }
182    /// # Ok(())
183    /// # }
184    /// ```
185    pub fn parse_auto(&self, path: &Path) -> Result<Vec<AgentEvent>> {
186        let path_str = path
187            .to_str()
188            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
189
190        let adapter = agtrace_providers::detect_adapter_from_path(path_str)
191            .map_err(|_| Error::NotFound("No suitable provider detected for file".to_string()))?;
192
193        adapter
194            .parser
195            .parse_file(path)
196            .map_err(|e| Error::InvalidInput(format!("Parse error: {}", e)))
197    }
198
199    /// Parse a log file with a specific provider.
200    ///
201    /// # Examples
202    ///
203    /// ```no_run
204    /// use agtrace_sdk::Providers;
205    /// use std::path::Path;
206    ///
207    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
208    /// let providers = Providers::detect()?;
209    /// let events = providers.parse_file(Path::new("/path/to/log.jsonl"), "claude_code")?;
210    /// # Ok(())
211    /// # }
212    /// ```
213    pub fn parse_file(&self, path: &Path, provider_name: &str) -> Result<Vec<AgentEvent>> {
214        let adapter = agtrace_providers::create_adapter(provider_name)
215            .map_err(|_| Error::NotFound(format!("Unknown provider: {}", provider_name)))?;
216
217        adapter
218            .parser
219            .parse_file(path)
220            .map_err(|e| Error::InvalidInput(format!("Parse error: {}", e)))
221    }
222
223    /// Run diagnostics on all configured providers.
224    ///
225    /// Scans each provider's log directory and reports parsing statistics.
226    ///
227    /// # Examples
228    ///
229    /// ```no_run
230    /// use agtrace_sdk::Providers;
231    ///
232    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
233    /// let providers = Providers::detect()?;
234    /// let results = providers.diagnose()?;
235    ///
236    /// for result in &results {
237    ///     let success_rate = if result.total_files > 0 {
238    ///         (result.successful as f64 / result.total_files as f64) * 100.0
239    ///     } else {
240    ///         100.0
241    ///     };
242    ///     println!("{}: {:.1}% success ({}/{})",
243    ///         result.provider_name,
244    ///         success_rate,
245    ///         result.successful,
246    ///         result.total_files);
247    /// }
248    /// # Ok(())
249    /// # }
250    /// ```
251    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
252        let providers: Vec<_> = self
253            .provider_configs
254            .iter()
255            .filter_map(|(name, path)| {
256                agtrace_providers::create_adapter(name)
257                    .ok()
258                    .map(|adapter| (adapter, path.clone()))
259            })
260            .collect();
261
262        agtrace_runtime::DoctorService::diagnose_all(&providers).map_err(Error::Runtime)
263    }
264
265    /// Check if a file can be parsed.
266    ///
267    /// # Examples
268    ///
269    /// ```no_run
270    /// use agtrace_sdk::Providers;
271    /// use std::path::Path;
272    ///
273    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
274    /// let providers = Providers::detect()?;
275    /// let result = providers.check_file(Path::new("/path/to/log.jsonl"), None)?;
276    /// println!("Status: {:?}", result.status);
277    /// # Ok(())
278    /// # }
279    /// ```
280    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
281        let path_str = path
282            .to_str()
283            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
284
285        let (adapter, provider_name) = if let Some(name) = provider {
286            let adapter = agtrace_providers::create_adapter(name)
287                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
288            (adapter, name.to_string())
289        } else {
290            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
291                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
292            let name = format!("{} (auto-detected)", adapter.id());
293            (adapter, name)
294        };
295
296        agtrace_runtime::DoctorService::check_file(path_str, &adapter, &provider_name)
297            .map_err(Error::Runtime)
298    }
299
300    /// Inspect raw file contents.
301    ///
302    /// Returns the first N lines of the file, optionally parsed as JSON.
303    ///
304    /// # Examples
305    ///
306    /// ```no_run
307    /// use agtrace_sdk::Providers;
308    /// use std::path::Path;
309    ///
310    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
311    /// let result = Providers::inspect_file(Path::new("/path/to/log.jsonl"), 10, true)?;
312    /// println!("Showing {} of {} lines", result.shown_lines, result.total_lines);
313    /// # Ok(())
314    /// # }
315    /// ```
316    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
317        let path_str = path
318            .to_str()
319            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
320
321        agtrace_runtime::DoctorService::inspect_file(path_str, lines, json_format)
322            .map_err(Error::Runtime)
323    }
324
325    /// List configured providers.
326    ///
327    /// # Examples
328    ///
329    /// ```no_run
330    /// use agtrace_sdk::Providers;
331    ///
332    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
333    /// let providers = Providers::detect()?;
334    /// for (name, config) in providers.list() {
335    ///     println!("{}: {:?}", name, config.log_root);
336    /// }
337    /// # Ok(())
338    /// # }
339    /// ```
340    pub fn list(&self) -> Vec<(&String, &ProviderConfig)> {
341        self.config.enabled_providers()
342    }
343
344    /// Get current configuration.
345    pub fn config(&self) -> &Config {
346        &self.config
347    }
348
349    /// Get provider configurations as (name, log_root) pairs.
350    pub fn provider_configs(&self) -> &[(String, PathBuf)] {
351        &self.provider_configs
352    }
353}
354
355// =============================================================================
356// ProvidersBuilder
357// =============================================================================
358
359/// Builder for configuring [`Providers`].
360///
361/// Allows programmatic configuration of providers without relying on
362/// filesystem detection or TOML files.
363///
364/// # Examples
365///
366/// ```no_run
367/// use agtrace_sdk::Providers;
368///
369/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
370/// // Start from auto-detected providers and add custom ones
371/// let providers = Providers::builder()
372///     .auto_detect()
373///     .provider("claude_code", "/custom/claude/path")
374///     .build()?;
375///
376/// // Or configure entirely manually
377/// let providers = Providers::builder()
378///     .provider("claude_code", "/path/to/.claude/projects")
379///     .provider("codex", "/path/to/.codex/sessions")
380///     .build()?;
381/// # Ok(())
382/// # }
383/// ```
384#[derive(Default)]
385pub struct ProvidersBuilder {
386    config: Option<Config>,
387    providers: Vec<(String, PathBuf)>,
388}
389
390impl ProvidersBuilder {
391    /// Create a new builder with no providers configured.
392    pub fn new() -> Self {
393        Self::default()
394    }
395
396    /// Load configuration from a TOML file.
397    ///
398    /// # Examples
399    ///
400    /// ```no_run
401    /// use agtrace_sdk::Providers;
402    /// use std::path::Path;
403    ///
404    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
405    /// let providers = Providers::builder()
406    ///     .config_file(Path::new("/path/to/config.toml"))?
407    ///     .build()?;
408    /// # Ok(())
409    /// # }
410    /// ```
411    pub fn config_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
412        let config = Config::load_from(&path.as_ref().to_path_buf()).map_err(Error::Runtime)?;
413        self.config = Some(config);
414        Ok(self)
415    }
416
417    /// Use explicit configuration.
418    pub fn config(mut self, config: Config) -> Self {
419        self.config = Some(config);
420        self
421    }
422
423    /// Add a provider with custom log root.
424    ///
425    /// This overrides any provider with the same name from config file
426    /// or auto-detection.
427    ///
428    /// # Examples
429    ///
430    /// ```no_run
431    /// use agtrace_sdk::Providers;
432    ///
433    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
434    /// let providers = Providers::builder()
435    ///     .provider("claude_code", "/custom/.claude/projects")
436    ///     .build()?;
437    /// # Ok(())
438    /// # }
439    /// ```
440    pub fn provider(mut self, name: &str, log_root: impl Into<PathBuf>) -> Self {
441        self.providers.push((name.to_string(), log_root.into()));
442        self
443    }
444
445    /// Enable auto-detection of providers.
446    ///
447    /// Scans default log directories for each supported provider
448    /// and enables those that exist.
449    pub fn auto_detect(mut self) -> Self {
450        match Config::detect_providers() {
451            Ok(config) => {
452                self.config = Some(config);
453            }
454            Err(_) => {
455                // Silently ignore detection errors
456            }
457        }
458        self
459    }
460
461    /// Build the `Providers` instance.
462    pub fn build(self) -> Result<Providers> {
463        let mut config = self.config.unwrap_or_default();
464
465        // Apply manual provider overrides
466        for (name, log_root) in self.providers {
467            config.set_provider(
468                name,
469                ProviderConfig {
470                    enabled: true,
471                    log_root,
472                    context_window_override: None,
473                },
474            );
475        }
476
477        Ok(Providers::with_config(config))
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn test_builder_creates_empty_providers() {
487        let providers = Providers::builder().build().unwrap();
488        assert!(providers.list().is_empty());
489    }
490
491    #[test]
492    fn test_builder_with_manual_provider() {
493        let providers = Providers::builder()
494            .provider("claude_code", "/tmp/test")
495            .build()
496            .unwrap();
497
498        assert_eq!(providers.list().len(), 1);
499        let (name, config) = &providers.list()[0];
500        assert_eq!(*name, "claude_code");
501        assert_eq!(config.log_root, PathBuf::from("/tmp/test"));
502    }
503
504    #[test]
505    fn test_with_config() {
506        let mut config = Config::default();
507        config.set_provider(
508            "test_provider".to_string(),
509            ProviderConfig {
510                enabled: true,
511                log_root: PathBuf::from("/test/path"),
512                context_window_override: None,
513            },
514        );
515
516        let providers = Providers::with_config(config);
517        assert_eq!(providers.list().len(), 1);
518    }
519}