#[cfg(not(sqlite_graphrag_miri))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::Parser;
use sqlite_graphrag::{
cli::Cli,
commands,
constants::{
CLI_LOCK_DEFAULT_WAIT_SECS, LLM_WORKER_RSS_MB, MAX_CONCURRENT_CLI_INSTANCES,
MIN_AVAILABLE_MEMORY_MB,
},
lock::acquire_cli_slot,
memory_guard::{available_memory_mb, calculate_safe_concurrency, check_available_memory},
storage::connection::register_vec_extension,
};
fn main() -> std::process::ExitCode {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
sqlite_graphrag::terminal::init_console();
let _reaper_report = sqlite_graphrag::reaper::scan_and_kill_orphans();
if std::env::var_os("RAYON_NUM_THREADS").is_none() {
unsafe {
std::env::set_var("RAYON_NUM_THREADS", "2");
}
}
let verbose_count: u8 = std::env::args()
.skip(1)
.map(|a| {
if a == "--verbose" || a == "-v" {
1u8
} else if a.starts_with("-v") && a.chars().skip(1).all(|c| c == 'v') {
(a.len() - 1).try_into().unwrap_or(u8::MAX)
} else {
0u8
}
})
.sum();
let log_level = if verbose_count > 0 {
match verbose_count {
1 => "info".to_string(),
2 => "debug".to_string(),
_ => "trace".to_string(),
}
} else {
std::env::var("SQLITE_GRAPHRAG_LOG_LEVEL").unwrap_or_else(|_| "warn".to_string())
};
let log_format =
std::env::var("SQLITE_GRAPHRAG_LOG_FORMAT").unwrap_or_else(|_| "pretty".to_string());
sqlite_graphrag::telemetry::init_tracing(&log_level, &log_format);
register_vec_extension();
#[cfg(feature = "deadlock-detection")]
{
std::thread::spawn(|| loop {
std::thread::sleep(std::time::Duration::from_secs(10));
let deadlocks = parking_lot::deadlock::check_deadlock();
if !deadlocks.is_empty() {
tracing::error!(target: "deadlock_detection", count = deadlocks.len(), "deadlocks detected");
for (i, threads) in deadlocks.iter().enumerate() {
for t in threads {
tracing::error!(
target: "deadlock_detection",
index = i,
thread_id = ?t.thread_id(),
backtrace = ?t.backtrace(),
"deadlock thread info"
);
}
}
}
});
}
{
let args: Vec<String> = std::env::args().collect();
let mut lang_override: Option<sqlite_graphrag::i18n::Language> = None;
let mut i = 1usize;
while i < args.len() {
if args[i] == "--lang" {
if let Some(val) = args.get(i + 1) {
lang_override = sqlite_graphrag::i18n::Language::from_str_opt(val);
}
i += 2;
} else if let Some(val) = args[i].strip_prefix("--lang=") {
lang_override = sqlite_graphrag::i18n::Language::from_str_opt(val);
i += 1;
} else {
i += 1;
}
}
sqlite_graphrag::i18n::init(lang_override);
}
let cli = Cli::parse();
sqlite_graphrag::i18n::init(cli.lang);
if let Some(dim) = cli.embedding_dim {
unsafe {
std::env::set_var("SQLITE_GRAPHRAG_EMBEDDING_DIM", dim.to_string());
}
}
if let Err(e) = sqlite_graphrag::tz::init(cli.tz) {
sqlite_graphrag::output::emit_error(&e.localized_message());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(e.exit_code() as u8);
}
if let Err(msg) = cli.validate_flags() {
sqlite_graphrag::output::emit_error(&msg);
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(2);
}
let embedding_heavy = cli.command.as_ref().is_some_and(|c| c.is_embedding_heavy());
let measured_available_mb = if embedding_heavy {
let available_mb = if cli.skip_memory_guard {
available_memory_mb()
} else {
match check_available_memory(MIN_AVAILABLE_MEMORY_MB) {
Ok(available_mb) => available_mb,
Err(e) => {
sqlite_graphrag::output::emit_error(&e.localized_message());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(e.exit_code() as u8);
}
}
};
Some(available_mb)
} else {
None
};
let requested_concurrency = cli.max_concurrency.unwrap_or(MAX_CONCURRENT_CLI_INSTANCES);
let max_concurrency = if embedding_heavy {
let cpu_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let available_mb = match measured_available_mb {
Some(mb) => mb,
None => {
sqlite_graphrag::output::emit_error_i18n(
"embedding-heavy command must measure available RAM",
&sqlite_graphrag::i18n::validation::runtime_pt::embedding_heavy_must_measure_ram(),
);
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(20);
}
};
let safe_concurrency = calculate_safe_concurrency(
available_mb,
cpu_count,
LLM_WORKER_RSS_MB,
MAX_CONCURRENT_CLI_INSTANCES,
);
let effective_concurrency = requested_concurrency.min(safe_concurrency);
sqlite_graphrag::output::emit_progress_i18n(
&format!(
"Heavy command detected; available memory: {available_mb} MB; safe concurrency: {safe_concurrency}"
),
&sqlite_graphrag::i18n::validation::runtime_pt::heavy_command_detected(
available_mb,
safe_concurrency,
),
);
if effective_concurrency < requested_concurrency {
sqlite_graphrag::output::emit_progress_i18n(
&format!(
"Reducing requested concurrency from {requested_concurrency} to {effective_concurrency} to avoid memory oversubscription"
),
&sqlite_graphrag::i18n::validation::runtime_pt::reducing_concurrency(
requested_concurrency,
effective_concurrency,
),
);
}
effective_concurrency
} else {
requested_concurrency.min(MAX_CONCURRENT_CLI_INSTANCES)
};
let wait_secs = cli.wait_lock.unwrap_or(CLI_LOCK_DEFAULT_WAIT_SECS);
if wait_secs > 5 {
tracing::info!(
wait_secs,
"cli slot acquire — using extended wait (cold-start headroom)"
);
}
let _slot_guard = if cli.command.as_ref().is_some_and(|c| c.uses_cli_slot()) {
Some(match acquire_cli_slot(max_concurrency, Some(wait_secs)) {
Ok(pair) => pair,
Err(e) => {
sqlite_graphrag::output::emit_error(&e.localized_message());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(e.exit_code() as u8);
}
})
} else {
None
};
sqlite_graphrag::signals::register_shutdown_handler();
if let Some(ref path) = cli.claude_binary {
std::env::set_var("SQLITE_GRAPHRAG_CLAUDE_BINARY", path);
}
if let Some(ref path) = cli.codex_binary {
std::env::set_var("SQLITE_GRAPHRAG_CODEX_BINARY", path);
}
if let Some(ref model) = cli.llm_model {
std::env::set_var("SQLITE_GRAPHRAG_LLM_MODEL", model);
}
if cli.skip_embedding_on_failure {
std::env::set_var("SQLITE_GRAPHRAG_SKIP_EMBEDDING_ON_FAILURE", "1");
}
if let Some(n) = cli.llm_max_host_concurrency {
std::env::set_var("SQLITE_GRAPHRAG_LLM_MAX_HOST_CONCURRENCY", n.to_string());
}
if let Some(secs) = cli.llm_slot_wait_secs {
std::env::set_var("SQLITE_GRAPHRAG_LLM_SLOT_WAIT_SECS", secs.to_string());
}
if cli.llm_slot_no_wait {
std::env::set_var("SQLITE_GRAPHRAG_LLM_SLOT_NO_WAIT", "1");
}
if cli.strict_env_clear {
std::env::set_var("SQLITE_GRAPHRAG_STRICT_ENV_CLEAR", "1");
}
std::env::set_var("SQLITE_GRAPHRAG_LLM_FALLBACK", &cli.llm_fallback);
{
use sqlite_graphrag::cli::EmbeddingBackendChoice;
let wants_openrouter = matches!(
cli.embedding_backend,
EmbeddingBackendChoice::Auto | EmbeddingBackendChoice::Openrouter
);
if wants_openrouter {
if matches!(cli.embedding_backend, EmbeddingBackendChoice::Openrouter)
&& cli.embedding_model.is_none()
{
let msg = "--embedding-backend openrouter requires --embedding-model (e.g. qwen/qwen3-embedding-8b)";
sqlite_graphrag::output::emit_error_json(78, msg);
sqlite_graphrag::output::emit_error(msg);
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(78_u8);
}
if let Some(model) = cli.embedding_model.as_deref() {
if let Some(resolved) = sqlite_graphrag::config::resolve_api_key(
"openrouter",
cli.openrouter_api_key.as_deref(),
) {
let dim = sqlite_graphrag::constants::embedding_dim();
if let Err(e) = sqlite_graphrag::embedder::get_openrouter_embedder(
resolved.value,
model,
dim,
) {
tracing::warn!(error = %e, "failed to initialise OpenRouter embedding client");
if matches!(cli.embedding_backend, EmbeddingBackendChoice::Openrouter) {
sqlite_graphrag::output::emit_error_json(78, &e.to_string());
sqlite_graphrag::output::emit_error(&e.to_string());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(78_u8);
}
}
} else if matches!(cli.embedding_backend, EmbeddingBackendChoice::Openrouter) {
let msg = "--embedding-backend openrouter requires OPENROUTER_API_KEY env var, config.toml key, or --openrouter-api-key flag";
sqlite_graphrag::output::emit_error_json(78, msg);
sqlite_graphrag::output::emit_error(msg);
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(78_u8);
}
}
}
}
if cli.dry_run_backend {
match commands::dry_run_backend::emit_dry_run_backend(&cli) {
Ok(()) => {
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::SUCCESS;
}
Err(e) => {
sqlite_graphrag::output::emit_error_json(e.exit_code(), &e.localized_message());
sqlite_graphrag::output::emit_error(&e.localized_message());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
return std::process::ExitCode::from(e.exit_code() as u8);
}
}
}
let result = match cli.command {
Some(cmd) => match cmd {
sqlite_graphrag::cli::Commands::Init(args) => {
commands::init::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Remember(args) => {
commands::remember::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::RememberBatch(args) => {
commands::remember_batch::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Ingest(args) => {
commands::ingest::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Recall(args) => {
commands::recall::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Edit(args) => {
commands::edit::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::History(args) => commands::history::run(args),
sqlite_graphrag::cli::Commands::Restore(args) => {
commands::restore::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::HybridSearch(args) => {
commands::hybrid_search::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Read(args) => commands::read::run(args),
sqlite_graphrag::cli::Commands::List(args) => commands::list::run(args),
sqlite_graphrag::cli::Commands::Forget(args) => commands::forget::run(args),
sqlite_graphrag::cli::Commands::Purge(args) => commands::purge::run(args),
sqlite_graphrag::cli::Commands::Rename(args) => commands::rename::run(args),
sqlite_graphrag::cli::Commands::Health(args) => commands::health::run(args),
sqlite_graphrag::cli::Commands::Migrate(args) => commands::migrate::run(args),
sqlite_graphrag::cli::Commands::NamespaceDetect(args) => {
commands::namespace_detect::run(args)
}
sqlite_graphrag::cli::Commands::Optimize(args) => commands::optimize::run(args),
sqlite_graphrag::cli::Commands::Stats(args) => commands::stats::run(args),
sqlite_graphrag::cli::Commands::SyncSafeCopy(args) => {
commands::sync_safe_copy::run(args)
}
sqlite_graphrag::cli::Commands::Backup(args) => commands::backup::run(args),
sqlite_graphrag::cli::Commands::Vacuum(args) => commands::vacuum::run(args),
sqlite_graphrag::cli::Commands::Link(args) => commands::link::run(args),
sqlite_graphrag::cli::Commands::Unlink(args) => commands::unlink::run(args),
sqlite_graphrag::cli::Commands::DeepResearch(args) => {
commands::deep_research::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::Related(args) => commands::related::run(args),
sqlite_graphrag::cli::Commands::Graph(args) => commands::graph_export::run(args),
sqlite_graphrag::cli::Commands::Export(args) => commands::export::run(args),
sqlite_graphrag::cli::Commands::Fts(args) => commands::fts::run(args),
sqlite_graphrag::cli::Commands::Vec(args) => commands::vec::run(args),
sqlite_graphrag::cli::Commands::CodexModels(_args) => {
let models = commands::codex_spawn::list_codex_models();
let payload = serde_json::json!({
"action": "codex_models",
"count": models.len(),
"models": models,
"default": "gpt-5.5",
});
sqlite_graphrag::output::emit_json_compact(&payload).and(Ok(()))
}
sqlite_graphrag::cli::Commands::PruneRelations(args) => {
commands::prune_relations::run(args)
}
sqlite_graphrag::cli::Commands::PruneNer(args) => commands::prune_ner::run(args),
sqlite_graphrag::cli::Commands::CleanupOrphans(args) => {
commands::cleanup_orphans::run(args)
}
sqlite_graphrag::cli::Commands::MemoryEntities(args) => {
commands::memory_entities::run(args)
}
sqlite_graphrag::cli::Commands::Cache(args) => commands::cache::run(args),
sqlite_graphrag::cli::Commands::DeleteEntity(args) => {
commands::delete_entity::run(args)
}
sqlite_graphrag::cli::Commands::Reclassify(args) => commands::reclassify::run(args),
sqlite_graphrag::cli::Commands::RenameEntity(args) => {
commands::rename_entity::run(args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::MergeEntities(args) => {
commands::merge_entities::run(args)
}
sqlite_graphrag::cli::Commands::Enrich(args) => {
commands::enrich::run(&args, cli.llm_backend, cli.embedding_backend)
}
sqlite_graphrag::cli::Commands::ReclassifyRelation(args) => {
commands::reclassify_relation::run(args)
}
sqlite_graphrag::cli::Commands::NormalizeEntities(args) => {
commands::normalize_entities::run(args)
}
sqlite_graphrag::cli::Commands::Completions(args) => commands::completions::run(args),
sqlite_graphrag::cli::Commands::DebugSchema(args) => commands::debug_schema::run(args),
sqlite_graphrag::cli::Commands::Slots(args) => commands::slots::run(args),
sqlite_graphrag::cli::Commands::Pending(args) => commands::pending::run(args),
sqlite_graphrag::cli::Commands::Embedding(args) => {
commands::embedding::run(args, cli.llm_backend)
}
sqlite_graphrag::cli::Commands::PendingEmbeddings(args) => {
commands::pending_embeddings::run(args)
}
sqlite_graphrag::cli::Commands::Config(args) => commands::config_cmd::run(args),
},
None => Ok(()),
};
if let Err(e) = result {
sqlite_graphrag::output::emit_error_json(e.exit_code(), &e.localized_message());
sqlite_graphrag::output::emit_error(&e.localized_message());
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
cleanup_spawn_dir();
return std::process::ExitCode::from(e.exit_code() as u8);
}
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = std::io::Write::flush(&mut std::io::stderr());
if sqlite_graphrag::shutdown_requested() {
cleanup_spawn_dir();
return std::process::ExitCode::from(sqlite_graphrag::constants::SHUTDOWN_EXIT_CODE as u8);
}
cleanup_spawn_dir();
std::process::ExitCode::SUCCESS
}
fn cleanup_spawn_dir() {
let dir = std::env::temp_dir().join(format!("sqlite-graphrag-spawn-{}", std::process::id()));
let _ = std::fs::remove_dir(&dir);
}