use crate::commands::*;
use crate::i18n::{current, Language};
use clap::{Parser, Subcommand};
fn max_concurrency_ceiling() -> usize {
std::thread::available_parallelism()
.map(|n| n.get() * 2)
.unwrap_or(8)
}
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum RelationKind {
AppliesTo,
Uses,
DependsOn,
Causes,
Fixes,
Contradicts,
Supports,
Follows,
Related,
Mentions,
Replaces,
TrackedIn,
}
impl RelationKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::AppliesTo => "applies_to",
Self::Uses => "uses",
Self::DependsOn => "depends_on",
Self::Causes => "causes",
Self::Fixes => "fixes",
Self::Contradicts => "contradicts",
Self::Supports => "supports",
Self::Follows => "follows",
Self::Related => "related",
Self::Mentions => "mentions",
Self::Replaces => "replaces",
Self::TrackedIn => "tracked_in",
}
}
}
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum GraphExportFormat {
Json,
Dot,
Mermaid,
}
#[derive(Parser)]
#[command(name = "sqlite-graphrag")]
#[command(version)]
#[command(about = "Local GraphRAG memory for LLMs in a single SQLite file")]
#[command(arg_required_else_help = true)]
pub struct Cli {
#[arg(long, global = true, value_name = "N")]
pub max_concurrency: Option<usize>,
#[arg(long, global = true, value_name = "SECONDS")]
pub wait_lock: Option<u64>,
#[arg(long, global = true, hide = true, default_value_t = false)]
pub skip_memory_guard: bool,
#[arg(long, global = true, value_enum, value_name = "LANG")]
pub lang: Option<crate::i18n::Language>,
#[arg(long, global = true, value_name = "IANA")]
pub tz: Option<chrono_tz::Tz>,
#[arg(short = 'v', long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[command(subcommand)]
pub command: Commands,
}
#[cfg(test)]
mod json_only_format_tests {
use super::Cli;
use clap::Parser;
#[test]
fn restore_accepts_only_format_json() {
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"restore",
"--name",
"mem",
"--version",
"1",
"--format",
"json",
])
.is_ok());
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"restore",
"--name",
"mem",
"--version",
"1",
"--format",
"text",
])
.is_err());
}
#[test]
fn hybrid_search_accepts_only_format_json() {
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"hybrid-search",
"query",
"--format",
"json",
])
.is_ok());
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"hybrid-search",
"query",
"--format",
"markdown",
])
.is_err());
}
#[test]
fn remember_recall_rename_vacuum_json_only() {
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"remember",
"--name",
"mem",
"--type",
"project",
"--description",
"desc",
"--format",
"json",
])
.is_ok());
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"remember",
"--name",
"mem",
"--type",
"project",
"--description",
"desc",
"--format",
"text",
])
.is_err());
assert!(
Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
.is_ok()
);
assert!(
Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
.is_err()
);
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"rename",
"--name",
"old",
"--new-name",
"new",
"--format",
"json",
])
.is_ok());
assert!(Cli::try_parse_from([
"sqlite-graphrag",
"rename",
"--name",
"old",
"--new-name",
"new",
"--format",
"markdown",
])
.is_err());
assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
}
}
impl Cli {
pub fn validate_flags(&self) -> Result<(), String> {
if let Some(n) = self.max_concurrency {
if n == 0 {
return Err(match current() {
Language::English => "--max-concurrency must be >= 1".to_string(),
Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
});
}
let teto = max_concurrency_ceiling();
if n > teto {
return Err(match current() {
Language::English => format!(
"--max-concurrency {n} exceeds the ceiling of {teto} (2Ă—nCPUs) on this system"
),
Language::Portuguese => format!(
"--max-concurrency {n} excede o teto de {teto} (2Ă—nCPUs) neste sistema"
),
});
}
}
Ok(())
}
}
impl Commands {
pub fn is_embedding_heavy(&self) -> bool {
matches!(
self,
Self::Init(_) | Self::Remember(_) | Self::Recall(_) | Self::HybridSearch(_)
)
}
pub fn uses_cli_slot(&self) -> bool {
!matches!(self, Self::Daemon(_))
}
}
#[derive(Subcommand)]
pub enum Commands {
#[command(after_long_help = "EXAMPLES:\n \
# Initialize in current directory (default behavior)\n \
sqlite-graphrag init\n\n \
# Initialize at a specific path\n \
sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n \
# Initialize using SQLITE_GRAPHRAG_HOME env var\n \
SQLITE_GRAPHRAG_HOME=/data sqlite-graphrag init")]
Init(init::InitArgs),
Daemon(daemon::DaemonArgs),
#[command(after_long_help = "EXAMPLES:\n \
# Inline body\n \
sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n \
# Body from file\n \
sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n \
# Body from stdin (pipe)\n \
cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n \
# Skip BERT entity extraction (faster)\n \
sqlite-graphrag remember --name quick --type note --description \"...\" --body \"...\" --skip-extraction")]
Remember(remember::RememberArgs),
Ingest(ingest::IngestArgs),
#[command(after_long_help = "EXAMPLES:\n \
# Top 10 semantic matches (default)\n \
sqlite-graphrag recall \"agent memory\"\n\n \
# Top 3 only\n \
sqlite-graphrag recall \"agent memory\" -k 3\n\n \
# Search across all namespaces\n \
sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n \
# Disable graph traversal (vector-only)\n \
sqlite-graphrag recall \"agent memory\" --no-graph")]
Recall(recall::RecallArgs),
Read(read::ReadArgs),
List(list::ListArgs),
Forget(forget::ForgetArgs),
Purge(purge::PurgeArgs),
Rename(rename::RenameArgs),
Edit(edit::EditArgs),
History(history::HistoryArgs),
Restore(restore::RestoreArgs),
#[command(after_long_help = "EXAMPLES:\n \
# Hybrid search combining KNN + FTS5 BM25 with RRF\n \
sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n \
# Custom weights for vector vs full-text components\n \
sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
HybridSearch(hybrid_search::HybridSearchArgs),
Health(health::HealthArgs),
Migrate(migrate::MigrateArgs),
NamespaceDetect(namespace_detect::NamespaceDetectArgs),
Optimize(optimize::OptimizeArgs),
Stats(stats::StatsArgs),
SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
Vacuum(vacuum::VacuumArgs),
Link(link::LinkArgs),
Unlink(unlink::UnlinkArgs),
Related(related::RelatedArgs),
Graph(graph_export::GraphArgs),
CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
Cache(cache::CacheArgs),
#[command(name = "__debug_schema", hide = true)]
DebugSchema(debug_schema::DebugSchemaArgs),
}
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum MemoryType {
User,
Feedback,
Project,
Reference,
Decision,
Incident,
Skill,
Document,
Note,
}
#[cfg(test)]
mod heavy_concurrency_tests {
use super::*;
#[test]
fn command_heavy_detects_init_and_embeddings() {
let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
assert!(init.command.is_embedding_heavy());
let remember = Cli::try_parse_from([
"sqlite-graphrag",
"remember",
"--name",
"test-memory",
"--type",
"project",
"--description",
"desc",
])
.expect("parse remember");
assert!(remember.command.is_embedding_heavy());
let recall =
Cli::try_parse_from(["sqlite-graphrag", "recall", "query"]).expect("parse recall");
assert!(recall.command.is_embedding_heavy());
let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "query"])
.expect("parse hybrid");
assert!(hybrid.command.is_embedding_heavy());
}
#[test]
fn command_light_does_not_mark_stats() {
let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
assert!(!stats.command.is_embedding_heavy());
}
}
impl MemoryType {
pub fn as_str(&self) -> &'static str {
match self {
Self::User => "user",
Self::Feedback => "feedback",
Self::Project => "project",
Self::Reference => "reference",
Self::Decision => "decision",
Self::Incident => "incident",
Self::Skill => "skill",
Self::Document => "document",
Self::Note => "note",
}
}
}