siteforge 0.1.19

Archive websites into AI-readable local knowledge archives
Documentation
//! Website archiving primitives and the high-level [`Siteforge`] facade.
//!
//! `siteforge` archives websites into file-first, AI-readable local knowledge
//! archives. The CLI and TUI are built on the same public modules exposed here;
//! programmatic callers can use [`Siteforge`] for the common archive, resume,
//! search, export, pack, and verify flows without manually stitching modules
//! together.
//!
//! Completed archives include readable Markdown, structured JSON, JSONL chunks,
//! agent entrypoints, compact crawl metadata, BLAKE3 checksums, and a
//! thesa-compatible `.thesa/` sidecar for external verification.
//!
//! # Example
//!
//! ```no_run
//! use siteforge::{Config, Siteforge};
//!
//! #[tokio::main]
//! async fn main() -> siteforge::Result<()> {
//!     let mut config = Config::default();
//!     config.archive_root = "archives".into();
//!
//!     let siteforge = Siteforge::new(config);
//!     let summary = siteforge.archive_page("https://example.com/").await?;
//!
//!     println!("{}", summary.archive_path.display());
//!     Ok(())
//! }
//! ```

pub mod archive;
pub mod cli;
pub mod config;
pub mod crawler;
pub mod errors;
pub mod export;
pub mod fetch;
pub mod ocr;
pub mod parse;
pub mod render;
pub mod search;
pub mod storage;
pub mod tui;

pub use archive::{ArchiveLayout, ArchiveManifest, CrawlStats, ThesaSidecarSummary};
pub use config::{ArchiveOutputProfile, Config};
pub use crawler::{
    ArchiveProgress, ArchiveProgressPhase, ArchiveProgressSender, ArchiveRunOptions, ArchiveSummary,
};
pub use errors::{Result, SiteforgeError};
pub use export::ExportFormat;
pub use storage::{SearchResult, StoredAsset, StoredPage};

#[derive(Debug, Clone)]
/// High-level programmatic interface over siteforge configuration and archive operations.
///
/// The facade owns a [`Config`] and clones it into async archive/resume runs.
/// Lower-level modules remain public for callers that need custom crawl,
/// storage, export, or parsing behavior.
pub struct Siteforge {
    config: Config,
}

impl Siteforge {
    /// Create a facade from an explicit configuration.
    pub fn new(config: Config) -> Self {
        Self { config }
    }

    /// Borrow the active configuration.
    pub fn config(&self) -> &Config {
        &self.config
    }

    /// Mutably borrow the active configuration before starting operations.
    pub fn config_mut(&mut self) -> &mut Config {
        &mut self.config
    }

    /// Resolve the archive layout for an archive ID under this facade's archive root.
    pub fn archive_layout(&self, archive_id: &str) -> Result<ArchiveLayout> {
        Ok(ArchiveLayout::new(
            self.config.resolved_archive_root()?,
            archive_id.to_string(),
        ))
    }

    /// Run a new archive job.
    ///
    /// This creates a new archive directory and fails if the target archive ID
    /// already exists. Use [`Self::resume`] for interrupted jobs.
    pub async fn archive(&self, options: ArchiveRunOptions) -> Result<ArchiveSummary> {
        crawler::run_archive(self.config.clone(), options).await
    }

    /// Archive exactly one URL using configuration defaults.
    pub async fn archive_page(&self, url: impl AsRef<str>) -> Result<ArchiveSummary> {
        let options = ArchiveRunOptions::single_page_url(url, &self.config)?;
        self.archive(options).await
    }

    /// Archive exactly one URL and send progress updates through a bounded channel.
    pub async fn archive_page_with_progress(
        &self,
        url: impl AsRef<str>,
        progress: ArchiveProgressSender,
    ) -> Result<ArchiveSummary> {
        let options = ArchiveRunOptions::single_page_url(url, &self.config)?;
        self.archive_with_progress(options, progress).await
    }

    /// Crawl one same-domain site from a seed URL using configuration defaults.
    pub async fn archive_full_site(&self, url: impl AsRef<str>) -> Result<ArchiveSummary> {
        let options = ArchiveRunOptions::full_site_url(url, &self.config)?;
        self.archive(options).await
    }

    /// Crawl one same-domain site and send progress updates through a bounded channel.
    pub async fn archive_full_site_with_progress(
        &self,
        url: impl AsRef<str>,
        progress: ArchiveProgressSender,
    ) -> Result<ArchiveSummary> {
        let options = ArchiveRunOptions::full_site_url(url, &self.config)?;
        self.archive_with_progress(options, progress).await
    }

    /// Run a new archive job and send progress updates through a bounded channel.
    pub async fn archive_with_progress(
        &self,
        options: ArchiveRunOptions,
        progress: ArchiveProgressSender,
    ) -> Result<ArchiveSummary> {
        crawler::run_archive_with_progress(self.config.clone(), options, progress).await
    }

    /// Resume an existing archive by archive ID.
    pub async fn resume(&self, archive_id: &str) -> Result<ArchiveSummary> {
        crawler::resume_archive(self.config.clone(), archive_id).await
    }

    /// Resume an existing archive and send progress updates through a bounded channel.
    pub async fn resume_with_progress(
        &self,
        archive_id: &str,
        progress: ArchiveProgressSender,
    ) -> Result<ArchiveSummary> {
        crawler::resume_archive_with_progress(self.config.clone(), archive_id, progress).await
    }

    /// List archive manifests under the configured archive root.
    pub fn list_archives(&self) -> Result<Vec<ArchiveManifest>> {
        archive::list_archives(&self.config)
    }

    /// Search one archive or all archives.
    ///
    /// Pass `Some(archive_id)` to limit results to one archive, or `None` to
    /// search all known archives under the configured archive root.
    pub fn search(
        &self,
        archive_id: Option<&str>,
        query: &str,
        limit: usize,
    ) -> Result<Vec<SearchResult>> {
        search::search_archives(&self.config, archive_id, query, limit)
    }

    /// Export an archive in one of the supported formats.
    ///
    /// If `output` is `None`, the export is written under the archive's
    /// `exports/` directory.
    pub fn export(
        &self,
        archive_id: &str,
        format: ExportFormat,
        output: Option<&std::path::Path>,
    ) -> Result<std::path::PathBuf> {
        export::export_archive(&self.config, archive_id, format, output)
    }

    /// Create token-budgeted AI context packs for an archive.
    pub fn create_ai_packs(
        &self,
        archive_id: &str,
        max_tokens: Option<usize>,
    ) -> Result<Vec<std::path::PathBuf>> {
        export::create_ai_packs(&self.config, archive_id, max_tokens)
    }

    /// Verify a siteforge archive by checksum manifest and required files.
    ///
    /// Returns an empty vector when verification succeeds. Any returned strings
    /// are human-readable verification problems.
    pub fn verify(&self, archive_id: &str) -> Result<Vec<String>> {
        archive::verify_archive(&self.config, archive_id)
    }
}

impl Default for Siteforge {
    fn default() -> Self {
        Self::new(Config::default())
    }
}

impl From<Config> for Siteforge {
    fn from(config: Config) -> Self {
        Self::new(config)
    }
}