vyctor 0.1.0

A fast CLI tool for semantic file search using vector embeddings
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};

mod cli;
mod config;
mod daemon;
mod embeddings;
mod indexer;
mod reranker;
mod search;
mod storage;

/// Load environment variables from .env file in the current directory or parent directories
fn load_dotenv() {
    // Try to find and load .env from current dir or parent directories
    if let Ok(cwd) = std::env::current_dir() {
        let mut dir = cwd.as_path();
        loop {
            let env_file = dir.join(".env");
            if env_file.exists() {
                let _ = dotenvy::from_path(&env_file);
                break;
            }
            // Also try .env.local
            let env_local = dir.join(".env.local");
            if env_local.exists() {
                let _ = dotenvy::from_path(&env_local);
            }
            match dir.parent() {
                Some(parent) => dir = parent,
                None => break,
            }
        }
    }
}

#[derive(Parser)]
#[command(name = "vyctor")]
#[command(
    author,
    version,
    about = "Fast semantic file search using vector embeddings"
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Initialize vyctor in the current directory
    Init {
        /// Force re-initialization even if .vyctor already exists
        #[arg(short, long)]
        force: bool,
    },
    /// Search for files matching a natural language query
    Lookup {
        /// The search query
        query: String,
        /// Folder to search in (relative to indexed root)
        #[arg(short, long)]
        folder: Option<String>,
        /// Number of results to return
        #[arg(short = 'n', long, default_value = "5")]
        count: usize,
        /// Show full file content instead of just the matching chunk
        #[arg(long)]
        full: bool,
        /// Show verbose output (model loading, detailed timing)
        #[arg(short, long)]
        verbose: bool,
    },
    /// Synchronize the index with current files
    Sync {
        /// Force re-index all files
        #[arg(short, long)]
        force: bool,
    },
    /// Watch for file changes and auto-sync
    Watch {
        /// Debounce interval in milliseconds
        #[arg(short, long, default_value = "300")]
        debounce: u64,
        /// Run as background daemon
        #[arg(long)]
        daemon: bool,
        /// Stop running daemon
        #[arg(long)]
        stop: bool,
        /// Show daemon status
        #[arg(long)]
        status: bool,
        /// Show daemon logs
        #[arg(long)]
        logs: bool,
        /// Follow log output (with --logs)
        #[arg(short, long)]
        follow: bool,
        /// Internal flag: this process is the daemon child
        #[arg(long, hide = true)]
        daemon_child: bool,
    },
    /// Show index status and statistics
    Status,
    /// Show or edit configuration
    Config {
        /// Open config in editor
        #[arg(short, long)]
        edit: bool,
    },
    /// Browse and analyze indexed files and chunks
    Browse {
        #[command(subcommand)]
        action: BrowseAction,
    },
}

#[derive(Subcommand)]
enum BrowseAction {
    /// List all indexed files
    Files {
        /// Filter files by path pattern
        #[arg(short, long)]
        filter: Option<String>,
        /// Show content hash for each file
        #[arg(long)]
        hash: bool,
    },
    /// Browse chunks
    Chunks {
        /// Show chunks for a specific file
        #[arg(short, long)]
        file: Option<String>,
        /// Show a specific chunk by ID
        #[arg(long)]
        id: Option<i64>,
        /// Page number (1-indexed)
        #[arg(short, long, default_value = "1")]
        page: usize,
        /// Number of chunks per page
        #[arg(short = 'n', long, default_value = "10")]
        size: usize,
        /// Show full chunk content
        #[arg(long)]
        full: bool,
    },
    /// Show index statistics by file type
    Stats,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Load .env file before anything else
    load_dotenv();

    let cli = Cli::parse();

    match cli.command {
        Commands::Init { force } => {
            cli::init::run(force).await?;
        }
        Commands::Lookup {
            query,
            folder,
            count,
            full,
            verbose,
        } => {
            cli::lookup::run(&query, folder.as_deref(), count, full, verbose).await?;
        }
        Commands::Sync { force } => {
            cli::sync::run(force).await?;
        }
        Commands::Watch {
            debounce,
            daemon,
            stop,
            status,
            logs,
            follow,
            daemon_child,
        } => {
            cli::watch::run(debounce, daemon, stop, status, logs, follow, daemon_child).await?;
        }
        Commands::Status => {
            cli::status::run().await?;
        }
        Commands::Config { edit } => {
            cli::config_cmd::run(edit).await?;
        }
        Commands::Browse { action } => match action {
            BrowseAction::Files { filter, hash } => {
                cli::browse::list_files(filter.as_deref(), hash).await?;
            }
            BrowseAction::Chunks {
                file,
                id,
                page,
                size,
                full,
            } => {
                cli::browse::show_chunks(file.as_deref(), id, page, size, full).await?;
            }
            BrowseAction::Stats => {
                cli::browse::show_stats().await?;
            }
        },
    }

    Ok(())
}