mod cli;
mod commands;
mod fs_atomic;
mod hooks_support;
mod render;
#[cfg(test)]
mod version_describe;
use std::fs;
use std::path::{Path, PathBuf};
use clap::Parser;
pub(crate) use commands::*;
pub(crate) use fs_atomic::*;
pub(crate) use hooks_support::*;
use rag_rat_core::index::IndexProgress;
use rag_rat_core::index::papertrail::PapertrailSyncAction;
use rag_rat_core::search::lexical::SearchHit;
use rag_rat_core::{Config, IndexDatabase};
pub(crate) use render::*;
use crate::cli::{Cli, Command as Cmd, DoctorArgs};
mod agent_hook;
mod init;
#[cfg(all(not(target_env = "msvc"), not(target_os = "android")))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[cfg(all(not(target_env = "msvc"), not(target_os = "android")))]
fn configure_jemalloc() {
use tikv_jemalloc_ctl::{background_thread, raw};
let _ = background_thread::write(true);
unsafe {
let _ = raw::write(b"arena.4096.dirty_decay_ms\0", 1000_isize);
let _ = raw::write(b"arena.4096.muzzy_decay_ms\0", 1000_isize);
let _ = raw::write(b"arenas.dirty_decay_ms\0", 1000_isize);
let _ = raw::write(b"arenas.muzzy_decay_ms\0", 1000_isize);
}
}
fn main() -> anyhow::Result<()> {
rag_rat_core::set_binary_version(env!("RAG_RAT_VERSION"));
#[cfg(all(not(target_env = "msvc"), not(target_os = "android")))]
configure_jemalloc();
let cli = Cli::parse();
set_output_format(if cli.json {
rag_rat_core::OutputFormat::Json
} else {
rag_rat_core::OutputFormat::Toon
});
match &cli.command {
Cmd::Init(args) => return init::run(args, cli.config.as_deref().unwrap_or("rag-rat.toml")),
Cmd::AgentHook => return agent_hook::run(),
Cmd::Mcp => return run_mcp(cli.config.as_deref(), cli.json),
Cmd::Doctor(args) => return run_doctor(args, cli.config.as_deref()),
_ => {},
}
let config = load_config_or_hint(cli.config.as_deref())?;
apply_embedding_runtime_env(&config.llm.embedding.runtime);
let _log = rag_rat_core::logging::init_logging(&config, log_role(&cli.command));
match cli.command {
Cmd::Init(_) | Cmd::AgentHook | Cmd::Mcp | Cmd::Doctor(_) =>
unreachable!("handled before the config load above"),
Cmd::Index(args) => index(&config, &args)?,
Cmd::Query(args) => query(&config, &args)?,
Cmd::Brief(args) => brief(&config, &args)?,
Cmd::Clusters(args) => clusters(&config, &args)?,
Cmd::ImportantSymbols(args) => important_symbols(&config, &args)?,
Cmd::Clones(args) => clones(&config, &args)?,
Cmd::ClonesFor(args) => clones_for(&config, &args)?,
Cmd::Memory(args) => memory(&config, &args)?,
Cmd::Dream(args) => dream(&config, &args)?,
Cmd::Github(args) => papertrail(&config, &args)?,
Cmd::Hooks(args) => hooks(&config, &args)?,
Cmd::Maintenance(args) => maintenance(&config, &args)?,
Cmd::Models(args) => models(&config, &args)?,
Cmd::Reconcile(args) => reconcile(&config, &args)?,
Cmd::Gc => {
let lock_repo = rag_rat_core::locks::write_lock_repo_id(&config);
let _lock =
rag_rat_core::locks::WriteLock::acquire_blocking(&config.database, &lock_repo)?;
let db = open_index(&config)?;
print_output(&db.garbage_collect()?)?;
},
#[cfg(feature = "eval")]
Cmd::Eval(args) => eval(&config, &args)?,
#[cfg(feature = "eval")]
Cmd::BenchmarkEmbedding(args) => benchmark_embedding(&config, &args)?,
Cmd::Oracle(args) => oracle(&config, &args)?,
Cmd::Consolidate => consolidate(&config)?,
Cmd::DumpConfig => dump_config(&config)?,
Cmd::VersionCheck => version_check(&config)?,
}
Ok(())
}
fn spawn_detached_version_refresh(config: &rag_rat_core::Config) {
use rag_rat_core::version_check;
const POLL: std::time::Duration = std::time::Duration::from_secs(6 * 60 * 60);
if !config.version_check.enabled {
return;
}
let database = config.database.clone();
std::thread::spawn(move || {
loop {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
if version_check::needs_refresh(
version_check::read_cache(&database).as_ref(),
now,
version_check::DEFAULT_TTL_MS,
) {
let _ = version_check::refresh(&database);
}
std::thread::sleep(POLL);
}
});
}
fn spawn_detached_oracle_auto_run(config: &rag_rat_core::Config) {
use rag_rat_core::index::oracle::{self, AutoRunDecision, AutoRunInputs, OracleTool};
if !config.oracle.auto_run {
return;
}
let quiet_secs = config.oracle.auto_run_quiet_period_secs;
let poll = std::time::Duration::from_secs((quiet_secs / 4).max(60));
let quiet_period_ms = saturating_secs_to_ms(quiet_secs);
let min_interval_ms = saturating_secs_to_ms(config.oracle.auto_run_min_interval_secs);
let config = config.clone();
std::thread::spawn(move || {
loop {
std::thread::sleep(poll);
let _ = maybe_run_oracle_once(&config, quiet_period_ms, min_interval_ms);
}
});
fn maybe_run_oracle_once(
config: &rag_rat_core::Config,
quiet_period_ms: i64,
min_interval_ms: i64,
) -> anyhow::Result<()> {
let now_ms = now_epoch_ms();
let last_index_change_ms = {
let db = open_index(config)?;
match db.status(&config.database)?.indexed_at_ms {
Some(ms) => ms,
None => return Ok(()),
}
};
let configured_languages: std::collections::HashSet<&str> =
config.targets.iter().map(|target| target.language.as_str()).collect();
for &tool in OracleTool::ALL {
let manifest = oracle::ToolManifest::for_tool(tool);
if !manifest.languages.iter().any(|lang| configured_languages.contains(lang)) {
continue;
}
if matches!(oracle::probe_oracle_tool(tool), oracle::ToolAvailability::Blocked { .. }) {
continue;
}
let last_run_ms = {
let db = open_index(config)?;
db.latest_oracle_run_started_at(tool)?
};
let decision = oracle::auto_run_decision(AutoRunInputs {
enabled: true,
now_ms,
last_index_change_ms,
last_run_ms,
quiet_period_ms,
min_interval_ms,
});
if decision == AutoRunDecision::Run {
let _ = run_oracle_tool_background(config, tool);
}
}
Ok(())
}
fn run_oracle_tool_background(
config: &rag_rat_core::Config,
tool: OracleTool,
) -> anyhow::Result<()> {
let (started_at_ms, pre_spawn_sha) = with_oracle_write_lock(config, |db| {
Ok((now_epoch_ms(), db.oracle_pre_spawn_snapshot()?))
})?;
let scip_output = config
.database
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(std::env::temp_dir)
.join(format!("rag-rat-oracle-auto-{}.scip", std::process::id()));
let production = oracle::produce_scip_with_tool(tool, &config.root, &scip_output);
let _ = fs::remove_file(&scip_output);
match production? {
oracle::ScipProduction::Blocked { .. } => Ok(()),
oracle::ScipProduction::Produced { version, bytes, production_sha } => {
with_oracle_write_lock(config, |db| {
db.run_oracle_at(
tool,
&version,
&bytes,
rag_rat_core::index::OracleShaSnapshots {
production: Some(&production_sha),
pre_spawn: Some(&pre_spawn_sha),
},
started_at_ms,
)
})?;
Ok(())
},
}
}
fn saturating_secs_to_ms(secs: u64) -> i64 {
i64::try_from(secs).unwrap_or(i64::MAX).saturating_mul(1000)
}
}
fn now_epoch_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
pub(crate) fn load_config_or_hint(explicit: Option<&str>) -> anyhow::Result<Config> {
match discover_config_optional(explicit)? {
Some(config) => Ok(config),
None => anyhow::bail!(
"No rag-rat config found at `{}`.\nRun `rag-rat init` to create one, or pass --config \
<path>.",
rag_rat_core::config::discover_config_path(Path::new(".")).display()
),
}
}
fn discover_config_optional(explicit: Option<&str>) -> anyhow::Result<Option<Config>> {
match explicit {
Some(path) => {
let path = PathBuf::from(path);
if !path.exists() {
anyhow::bail!(
"No rag-rat config found at `{}`.\nRun `rag-rat init` to create one, or pass \
--config <path>.",
path.display()
);
}
Ok(Some(Config::load(path)?))
},
None => {
let path = rag_rat_core::config::discover_config_path(Path::new("."));
if !path.exists() {
return Ok(None);
}
Ok(Some(Config::load(path)?))
},
}
}
fn run_mcp(explicit: Option<&str>, json: bool) -> anyhow::Result<()> {
let output_format =
if json { rag_rat_core::OutputFormat::Json } else { rag_rat_core::OutputFormat::Toon };
let config = discover_config_optional(explicit)?;
let _log = config.as_ref().map(|config| {
apply_embedding_runtime_env(&config.llm.embedding.runtime);
rag_rat_core::logging::init_logging(config, rag_rat_core::logging::Role::Mcp)
});
if let Some(config) = &config {
spawn_detached_version_refresh(config);
spawn_detached_oracle_auto_run(config);
}
let runtime =
tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build()?;
match config {
Some(config) => runtime.block_on(rag_rat_mcp::server::run_stdio(config, output_format))?,
None => runtime.block_on(rag_rat_mcp::server::run_stdio_dormant(output_format))?,
}
Ok(())
}
fn run_doctor(args: &DoctorArgs, explicit: Option<&str>) -> anyhow::Result<()> {
if let Some(config) = discover_config_optional(explicit)? {
let _log = rag_rat_core::logging::init_logging(
&config,
rag_rat_core::logging::Role::Cli("doctor".to_string()),
);
return doctor(&config, args);
}
if args.vacuum {
anyhow::bail!(
"`rag-rat doctor --vacuum` needs a rag-rat repo (it rewrites that repo's index).\nRun \
it from the repo checkout, or pass --config <path>."
);
}
match rag_rat_core::data_dir::global_database_path() {
Some(database) => doctor_global_store(&database),
None => anyhow::bail!(
"No rag-rat config found at `{}`, and this platform has no data directory for a \
machine-global store.\nRun `rag-rat init` to create a config, or pass --config \
<path>.",
rag_rat_core::config::discover_config_path(Path::new(".")).display()
),
}
}
fn log_role(cmd: &Cmd) -> rag_rat_core::logging::Role {
use rag_rat_core::logging::Role;
match cmd {
Cmd::Mcp => Role::Mcp,
Cmd::Maintenance(args) if is_git_hook_trigger(args.trigger.as_deref()) => Role::Hook,
Cmd::Maintenance(_) => Role::Cli("maintenance".to_string()),
Cmd::Reconcile(_) => Role::Cli("reconcile".to_string()),
Cmd::Index(_) => Role::Cli("index".to_string()),
Cmd::Doctor(_) => Role::Cli("doctor".to_string()),
Cmd::Gc => Role::Cli("gc".to_string()),
_ => Role::Cli("cmd".to_string()),
}
}
fn is_git_hook_trigger(trigger: Option<&str>) -> bool {
matches!(trigger, Some("post-commit" | "post-checkout" | "post-merge" | "post-rewrite"))
}
pub(crate) fn open_index(config: &Config) -> anyhow::Result<IndexDatabase> {
if !config.database.exists() {
anyhow::bail!(
"No index found at {}.\nRun `rag-rat index` to build it first.",
config.database.display()
);
}
IndexDatabase::open_config(config)
}
pub(crate) const MANAGED_HOOKS: &[&str] =
&["post-checkout", "post-merge", "post-rewrite", "post-commit"];
const HOOK_MARKER: &str = "# Generated by rag-rat.";
const DEFAULT_MAINTENANCE_SECONDS: u64 = 30;
#[derive(Debug)]
pub(crate) struct GitPaths {
worktree_root: PathBuf,
git_dir: PathBuf,
git_common_dir: PathBuf,
pub(crate) hooks_dir: PathBuf,
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
use super::{load_config_or_hint, progress_percent};
static TMP: AtomicU64 = AtomicU64::new(0);
#[test]
fn progress_percent_is_capped() {
assert_eq!(progress_percent(0, 0), 100);
assert_eq!(progress_percent(50, 100), 50);
assert_eq!(progress_percent(17_024, 11_998), 100);
}
#[test]
fn missing_config_yields_friendly_init_hint() {
let n = TMP.fetch_add(1, Ordering::Relaxed);
let missing =
std::env::temp_dir().join(format!("rag-rat-no-config-{}-{n}.toml", std::process::id()));
let _ = std::fs::remove_file(&missing);
let err = load_config_or_hint(Some(missing.to_str().unwrap())).unwrap_err();
let message = err.to_string();
assert!(message.contains("rag-rat init"), "expected init hint, got: {message}");
}
}