mod cli;
mod commands;
mod fs_atomic;
mod hooks_support;
mod render;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
use std::{env, fs};
use clap::Parser;
pub(crate) use commands::*;
pub(crate) use fs_atomic::*;
pub(crate) use hooks_support::*;
use rag_rat_core::config::EmbeddingRuntimeConfig;
use rag_rat_core::index::IndexProgress;
use rag_rat_core::index::github::GitHubSyncAction;
use rag_rat_core::search::lexical::SearchHit;
use rag_rat_core::{Config, IndexDatabase};
pub(crate) use render::*;
use crate::cli::{Cli, Command as Cmd};
mod claude_hook;
mod claude_settings;
mod init;
fn main() -> anyhow::Result<()> {
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),
Cmd::ClaudeHook => return claude_hook::run(),
_ => {},
}
let config = load_config_or_hint(&cli.config)?;
apply_embedding_runtime_env(&config.local_ai.embedding.runtime);
match cli.command {
Cmd::Init(_) | Cmd::ClaudeHook => unreachable!("handled before the config load above"),
Cmd::Index(args) => index(&config, &args)?,
Cmd::Doctor => doctor(&config)?,
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::Mcp => {
spawn_detached_version_refresh(&config);
spawn_detached_oracle_auto_run(&config);
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?
.block_on(rag_rat_mcp::server::run_stdio(
config,
if cli.json {
rag_rat_core::OutputFormat::Json
} else {
rag_rat_core::OutputFormat::Toon
},
))?;
},
Cmd::Memory(args) => memory(&config, &args)?,
Cmd::Github(args) => github(&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 db = open_index(&config)?;
print_output(&db.garbage_collect()?)?;
},
Cmd::Eval(args) => eval(&config, &args)?,
Cmd::Oracle(args) => oracle(&config, &args)?,
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(()),
}
};
for &tool in OracleTool::ALL {
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(path: &str) -> anyhow::Result<Config> {
if !Path::new(path).exists() {
anyhow::bail!(
"No rag-rat config found at `{path}`.\nRun `rag-rat init` to create one, or pass \
--config <path>."
);
}
Ok(Config::load(path)?)
}
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(missing.to_str().unwrap()).unwrap_err();
let message = err.to_string();
assert!(message.contains("rag-rat init"), "expected init hint, got: {message}");
}
}