1use crate::commands::*;
6use crate::i18n::{current, Language};
7use clap::{Parser, Subcommand};
8
9fn max_concurrency_ceiling() -> usize {
11 std::thread::available_parallelism()
12 .map(|n| n.get() * 2)
13 .unwrap_or(8)
14}
15
16#[derive(Copy, Clone, Debug, clap::ValueEnum)]
17pub enum RelationKind {
18 AppliesTo,
19 Uses,
20 DependsOn,
21 Causes,
22 Fixes,
23 Contradicts,
24 Supports,
25 Follows,
26 Related,
27 Mentions,
28 Replaces,
29 TrackedIn,
30}
31
32impl RelationKind {
33 pub fn as_str(&self) -> &'static str {
34 match self {
35 Self::AppliesTo => "applies_to",
36 Self::Uses => "uses",
37 Self::DependsOn => "depends_on",
38 Self::Causes => "causes",
39 Self::Fixes => "fixes",
40 Self::Contradicts => "contradicts",
41 Self::Supports => "supports",
42 Self::Follows => "follows",
43 Self::Related => "related",
44 Self::Mentions => "mentions",
45 Self::Replaces => "replaces",
46 Self::TrackedIn => "tracked_in",
47 }
48 }
49}
50
51#[derive(Copy, Clone, Debug, clap::ValueEnum)]
52pub enum GraphExportFormat {
53 Json,
54 Dot,
55 Mermaid,
56}
57
58#[derive(Parser)]
59#[command(name = "sqlite-graphrag")]
60#[command(version)]
61#[command(about = "Local GraphRAG memory for LLMs in a single SQLite file")]
62#[command(arg_required_else_help = true)]
63pub struct Cli {
64 #[arg(long, global = true, value_name = "N")]
69 pub max_concurrency: Option<usize>,
70
71 #[arg(long, global = true, value_name = "SECONDS")]
76 pub wait_lock: Option<u64>,
77
78 #[arg(long, global = true, hide = true, default_value_t = false)]
82 pub skip_memory_guard: bool,
83
84 #[arg(long, global = true, value_enum, value_name = "LANG")]
90 pub lang: Option<crate::i18n::Language>,
91
92 #[arg(long, global = true, value_name = "IANA")]
98 pub tz: Option<chrono_tz::Tz>,
99
100 #[arg(short = 'v', long, global = true, action = clap::ArgAction::Count)]
105 pub verbose: u8,
106
107 #[command(subcommand)]
108 pub command: Commands,
109}
110
111#[cfg(test)]
112mod testes_formato_json_only {
113 use super::Cli;
114 use clap::Parser;
115
116 #[test]
117 fn restore_aceita_apenas_format_json() {
118 assert!(Cli::try_parse_from([
119 "sqlite-graphrag",
120 "restore",
121 "--name",
122 "mem",
123 "--version",
124 "1",
125 "--format",
126 "json",
127 ])
128 .is_ok());
129
130 assert!(Cli::try_parse_from([
131 "sqlite-graphrag",
132 "restore",
133 "--name",
134 "mem",
135 "--version",
136 "1",
137 "--format",
138 "text",
139 ])
140 .is_err());
141 }
142
143 #[test]
144 fn hybrid_search_aceita_apenas_format_json() {
145 assert!(Cli::try_parse_from([
146 "sqlite-graphrag",
147 "hybrid-search",
148 "query",
149 "--format",
150 "json",
151 ])
152 .is_ok());
153
154 assert!(Cli::try_parse_from([
155 "sqlite-graphrag",
156 "hybrid-search",
157 "query",
158 "--format",
159 "markdown",
160 ])
161 .is_err());
162 }
163
164 #[test]
165 fn remember_recall_rename_vacuum_json_only() {
166 assert!(Cli::try_parse_from([
167 "sqlite-graphrag",
168 "remember",
169 "--name",
170 "mem",
171 "--type",
172 "project",
173 "--description",
174 "desc",
175 "--format",
176 "json",
177 ])
178 .is_ok());
179 assert!(Cli::try_parse_from([
180 "sqlite-graphrag",
181 "remember",
182 "--name",
183 "mem",
184 "--type",
185 "project",
186 "--description",
187 "desc",
188 "--format",
189 "text",
190 ])
191 .is_err());
192
193 assert!(
194 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
195 .is_ok()
196 );
197 assert!(
198 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
199 .is_err()
200 );
201
202 assert!(Cli::try_parse_from([
203 "sqlite-graphrag",
204 "rename",
205 "--name",
206 "old",
207 "--new-name",
208 "new",
209 "--format",
210 "json",
211 ])
212 .is_ok());
213 assert!(Cli::try_parse_from([
214 "sqlite-graphrag",
215 "rename",
216 "--name",
217 "old",
218 "--new-name",
219 "new",
220 "--format",
221 "markdown",
222 ])
223 .is_err());
224
225 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
226 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
227 }
228}
229
230impl Cli {
231 pub fn validate_flags(&self) -> Result<(), String> {
236 if let Some(n) = self.max_concurrency {
237 if n == 0 {
238 return Err(match current() {
239 Language::English => "--max-concurrency must be >= 1".to_string(),
240 Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
241 });
242 }
243 let teto = max_concurrency_ceiling();
244 if n > teto {
245 return Err(match current() {
246 Language::English => format!(
247 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
248 ),
249 Language::Portuguese => format!(
250 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
251 ),
252 });
253 }
254 }
255 Ok(())
256 }
257}
258
259impl Commands {
260 pub fn is_embedding_heavy(&self) -> bool {
262 matches!(
263 self,
264 Self::Init(_) | Self::Remember(_) | Self::Recall(_) | Self::HybridSearch(_)
265 )
266 }
267
268 pub fn uses_cli_slot(&self) -> bool {
269 !matches!(self, Self::Daemon(_))
270 }
271}
272
273#[derive(Subcommand)]
274pub enum Commands {
275 #[command(after_long_help = "EXAMPLES:\n \
277 # Initialize in current directory (default behavior)\n \
278 sqlite-graphrag init\n\n \
279 # Initialize at a specific path\n \
280 sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n \
281 # Initialize using SQLITE_GRAPHRAG_HOME env var\n \
282 SQLITE_GRAPHRAG_HOME=/data sqlite-graphrag init")]
283 Init(init::InitArgs),
284 Daemon(daemon::DaemonArgs),
286 #[command(after_long_help = "EXAMPLES:\n \
288 # Inline body\n \
289 sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n \
290 # Body from file\n \
291 sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n \
292 # Body from stdin (pipe)\n \
293 cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n \
294 # Skip BERT entity extraction (faster)\n \
295 sqlite-graphrag remember --name quick --type note --description \"...\" --body \"...\" --skip-extraction")]
296 Remember(remember::RememberArgs),
297 #[command(after_long_help = "EXAMPLES:\n \
299 # Top 10 semantic matches (default)\n \
300 sqlite-graphrag recall \"agent memory\"\n\n \
301 # Top 3 only\n \
302 sqlite-graphrag recall \"agent memory\" -k 3\n\n \
303 # Search across all namespaces\n \
304 sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n \
305 # Disable graph traversal (vector-only)\n \
306 sqlite-graphrag recall \"agent memory\" --no-graph")]
307 Recall(recall::RecallArgs),
308 Read(read::ReadArgs),
310 List(list::ListArgs),
312 Forget(forget::ForgetArgs),
314 Purge(purge::PurgeArgs),
316 Rename(rename::RenameArgs),
318 Edit(edit::EditArgs),
320 History(history::HistoryArgs),
322 Restore(restore::RestoreArgs),
324 #[command(after_long_help = "EXAMPLES:\n \
326 # Hybrid search combining KNN + FTS5 BM25 with RRF\n \
327 sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n \
328 # Custom weights for vector vs full-text components\n \
329 sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
330 HybridSearch(hybrid_search::HybridSearchArgs),
331 Health(health::HealthArgs),
333 Migrate(migrate::MigrateArgs),
335 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
337 Optimize(optimize::OptimizeArgs),
339 Stats(stats::StatsArgs),
341 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
343 Vacuum(vacuum::VacuumArgs),
345 Link(link::LinkArgs),
347 Unlink(unlink::UnlinkArgs),
349 Related(related::RelatedArgs),
351 Graph(graph_export::GraphArgs),
353 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
355 Cache(cache::CacheArgs),
357 #[command(name = "__debug_schema", hide = true)]
358 DebugSchema(debug_schema::DebugSchemaArgs),
359}
360
361#[derive(Copy, Clone, Debug, clap::ValueEnum)]
362pub enum MemoryType {
363 User,
364 Feedback,
365 Project,
366 Reference,
367 Decision,
368 Incident,
369 Skill,
370 Document,
371 Note,
372}
373
374#[cfg(test)]
375mod testes_concorrencia_pesada {
376 use super::*;
377
378 #[test]
379 fn command_heavy_detecta_init_e_embeddings() {
380 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
381 assert!(init.command.is_embedding_heavy());
382
383 let remember = Cli::try_parse_from([
384 "sqlite-graphrag",
385 "remember",
386 "--name",
387 "memoria-teste",
388 "--type",
389 "project",
390 "--description",
391 "desc",
392 ])
393 .expect("parse remember");
394 assert!(remember.command.is_embedding_heavy());
395
396 let recall =
397 Cli::try_parse_from(["sqlite-graphrag", "recall", "consulta"]).expect("parse recall");
398 assert!(recall.command.is_embedding_heavy());
399
400 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "consulta"])
401 .expect("parse hybrid");
402 assert!(hybrid.command.is_embedding_heavy());
403 }
404
405 #[test]
406 fn command_light_nao_marca_stats() {
407 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
408 assert!(!stats.command.is_embedding_heavy());
409 }
410}
411
412impl MemoryType {
413 pub fn as_str(&self) -> &'static str {
414 match self {
415 Self::User => "user",
416 Self::Feedback => "feedback",
417 Self::Project => "project",
418 Self::Reference => "reference",
419 Self::Decision => "decision",
420 Self::Incident => "incident",
421 Self::Skill => "skill",
422 Self::Document => "document",
423 Self::Note => "note",
424 }
425 }
426}