1use crate::commands::*;
2use crate::i18n::{current, Language};
3use clap::{Parser, Subcommand};
4
5fn max_concurrency_ceiling() -> usize {
7 std::thread::available_parallelism()
8 .map(|n| n.get() * 2)
9 .unwrap_or(8)
10}
11
12#[derive(Copy, Clone, Debug, clap::ValueEnum)]
13pub enum RelationKind {
14 AppliesTo,
15 Uses,
16 DependsOn,
17 Causes,
18 Fixes,
19 Contradicts,
20 Supports,
21 Follows,
22 Related,
23 Mentions,
24 Replaces,
25 TrackedIn,
26}
27
28impl RelationKind {
29 pub fn as_str(&self) -> &'static str {
30 match self {
31 Self::AppliesTo => "applies_to",
32 Self::Uses => "uses",
33 Self::DependsOn => "depends_on",
34 Self::Causes => "causes",
35 Self::Fixes => "fixes",
36 Self::Contradicts => "contradicts",
37 Self::Supports => "supports",
38 Self::Follows => "follows",
39 Self::Related => "related",
40 Self::Mentions => "mentions",
41 Self::Replaces => "replaces",
42 Self::TrackedIn => "tracked_in",
43 }
44 }
45}
46
47#[derive(Copy, Clone, Debug, clap::ValueEnum)]
48pub enum GraphExportFormat {
49 Json,
50 Dot,
51 Mermaid,
52}
53
54#[derive(Parser)]
55#[command(name = "sqlite-graphrag")]
56#[command(version)]
57#[command(about = "Local GraphRAG memory for LLMs in a single SQLite file")]
58#[command(arg_required_else_help = true)]
59pub struct Cli {
60 #[arg(long, global = true, value_name = "N")]
65 pub max_concurrency: Option<usize>,
66
67 #[arg(long, global = true, value_name = "SECONDS")]
72 pub wait_lock: Option<u64>,
73
74 #[arg(long, global = true, hide = true, default_value_t = false)]
78 pub skip_memory_guard: bool,
79
80 #[arg(long, global = true, value_enum, value_name = "LANG")]
86 pub lang: Option<crate::i18n::Language>,
87
88 #[arg(long, global = true, value_name = "IANA")]
93 pub tz: Option<chrono_tz::Tz>,
94
95 #[command(subcommand)]
96 pub command: Commands,
97}
98
99#[cfg(test)]
100mod testes_formato_json_only {
101 use super::Cli;
102 use clap::Parser;
103
104 #[test]
105 fn restore_aceita_apenas_format_json() {
106 assert!(Cli::try_parse_from([
107 "sqlite-graphrag",
108 "restore",
109 "--name",
110 "mem",
111 "--version",
112 "1",
113 "--format",
114 "json",
115 ])
116 .is_ok());
117
118 assert!(Cli::try_parse_from([
119 "sqlite-graphrag",
120 "restore",
121 "--name",
122 "mem",
123 "--version",
124 "1",
125 "--format",
126 "text",
127 ])
128 .is_err());
129 }
130
131 #[test]
132 fn hybrid_search_aceita_apenas_format_json() {
133 assert!(Cli::try_parse_from([
134 "sqlite-graphrag",
135 "hybrid-search",
136 "query",
137 "--format",
138 "json",
139 ])
140 .is_ok());
141
142 assert!(Cli::try_parse_from([
143 "sqlite-graphrag",
144 "hybrid-search",
145 "query",
146 "--format",
147 "markdown",
148 ])
149 .is_err());
150 }
151
152 #[test]
153 fn remember_recall_rename_vacuum_json_only() {
154 assert!(Cli::try_parse_from([
155 "sqlite-graphrag",
156 "remember",
157 "--name",
158 "mem",
159 "--type",
160 "project",
161 "--description",
162 "desc",
163 "--format",
164 "json",
165 ])
166 .is_ok());
167 assert!(Cli::try_parse_from([
168 "sqlite-graphrag",
169 "remember",
170 "--name",
171 "mem",
172 "--type",
173 "project",
174 "--description",
175 "desc",
176 "--format",
177 "text",
178 ])
179 .is_err());
180
181 assert!(
182 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
183 .is_ok()
184 );
185 assert!(
186 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
187 .is_err()
188 );
189
190 assert!(Cli::try_parse_from([
191 "sqlite-graphrag",
192 "rename",
193 "--name",
194 "old",
195 "--new-name",
196 "new",
197 "--format",
198 "json",
199 ])
200 .is_ok());
201 assert!(Cli::try_parse_from([
202 "sqlite-graphrag",
203 "rename",
204 "--name",
205 "old",
206 "--new-name",
207 "new",
208 "--format",
209 "markdown",
210 ])
211 .is_err());
212
213 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
214 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
215 }
216}
217
218impl Cli {
219 pub fn validate_flags(&self) -> Result<(), String> {
224 if let Some(n) = self.max_concurrency {
225 if n == 0 {
226 return Err(match current() {
227 Language::English => "--max-concurrency must be >= 1".to_string(),
228 Language::Portugues => "--max-concurrency deve ser >= 1".to_string(),
229 });
230 }
231 let teto = max_concurrency_ceiling();
232 if n > teto {
233 return Err(match current() {
234 Language::English => format!(
235 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
236 ),
237 Language::Portugues => format!(
238 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
239 ),
240 });
241 }
242 }
243 Ok(())
244 }
245}
246
247impl Commands {
248 pub fn is_embedding_heavy(&self) -> bool {
250 matches!(
251 self,
252 Self::Init(_) | Self::Remember(_) | Self::Recall(_) | Self::HybridSearch(_)
253 )
254 }
255}
256
257#[derive(Subcommand)]
258pub enum Commands {
259 Init(init::InitArgs),
261 Remember(remember::RememberArgs),
263 Recall(recall::RecallArgs),
265 Read(read::ReadArgs),
267 List(list::ListArgs),
269 Forget(forget::ForgetArgs),
271 Purge(purge::PurgeArgs),
273 Rename(rename::RenameArgs),
275 Edit(edit::EditArgs),
277 History(history::HistoryArgs),
279 Restore(restore::RestoreArgs),
281 HybridSearch(hybrid_search::HybridSearchArgs),
283 Health(health::HealthArgs),
285 Migrate(migrate::MigrateArgs),
287 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
289 Optimize(optimize::OptimizeArgs),
291 Stats(stats::StatsArgs),
293 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
295 Vacuum(vacuum::VacuumArgs),
297 Link(link::LinkArgs),
299 Unlink(unlink::UnlinkArgs),
301 Related(related::RelatedArgs),
303 Graph(graph_export::GraphArgs),
305 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
307 #[command(name = "__debug_schema", hide = true)]
308 DebugSchema(debug_schema::DebugSchemaArgs),
309}
310
311#[derive(Copy, Clone, Debug, clap::ValueEnum)]
312pub enum MemoryType {
313 User,
314 Feedback,
315 Project,
316 Reference,
317 Decision,
318 Incident,
319 Skill,
320}
321
322#[cfg(test)]
323mod testes_concorrencia_pesada {
324 use super::*;
325
326 #[test]
327 fn command_heavy_detecta_init_e_embeddings() {
328 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
329 assert!(init.command.is_embedding_heavy());
330
331 let remember = Cli::try_parse_from([
332 "sqlite-graphrag",
333 "remember",
334 "--name",
335 "memoria-teste",
336 "--type",
337 "project",
338 "--description",
339 "desc",
340 ])
341 .expect("parse remember");
342 assert!(remember.command.is_embedding_heavy());
343
344 let recall =
345 Cli::try_parse_from(["sqlite-graphrag", "recall", "consulta"]).expect("parse recall");
346 assert!(recall.command.is_embedding_heavy());
347
348 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "consulta"])
349 .expect("parse hybrid");
350 assert!(hybrid.command.is_embedding_heavy());
351 }
352
353 #[test]
354 fn command_light_nao_marca_stats() {
355 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
356 assert!(!stats.command.is_embedding_heavy());
357 }
358}
359
360impl MemoryType {
361 pub fn as_str(&self) -> &'static str {
362 match self {
363 Self::User => "user",
364 Self::Feedback => "feedback",
365 Self::Project => "project",
366 Self::Reference => "reference",
367 Self::Decision => "decision",
368 Self::Incident => "incident",
369 Self::Skill => "skill",
370 }
371 }
372}