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")]
94 pub tz: Option<chrono_tz::Tz>,
95
96 #[command(subcommand)]
97 pub command: Commands,
98}
99
100#[cfg(test)]
101mod testes_formato_json_only {
102 use super::Cli;
103 use clap::Parser;
104
105 #[test]
106 fn restore_aceita_apenas_format_json() {
107 assert!(Cli::try_parse_from([
108 "sqlite-graphrag",
109 "restore",
110 "--name",
111 "mem",
112 "--version",
113 "1",
114 "--format",
115 "json",
116 ])
117 .is_ok());
118
119 assert!(Cli::try_parse_from([
120 "sqlite-graphrag",
121 "restore",
122 "--name",
123 "mem",
124 "--version",
125 "1",
126 "--format",
127 "text",
128 ])
129 .is_err());
130 }
131
132 #[test]
133 fn hybrid_search_aceita_apenas_format_json() {
134 assert!(Cli::try_parse_from([
135 "sqlite-graphrag",
136 "hybrid-search",
137 "query",
138 "--format",
139 "json",
140 ])
141 .is_ok());
142
143 assert!(Cli::try_parse_from([
144 "sqlite-graphrag",
145 "hybrid-search",
146 "query",
147 "--format",
148 "markdown",
149 ])
150 .is_err());
151 }
152
153 #[test]
154 fn remember_recall_rename_vacuum_json_only() {
155 assert!(Cli::try_parse_from([
156 "sqlite-graphrag",
157 "remember",
158 "--name",
159 "mem",
160 "--type",
161 "project",
162 "--description",
163 "desc",
164 "--format",
165 "json",
166 ])
167 .is_ok());
168 assert!(Cli::try_parse_from([
169 "sqlite-graphrag",
170 "remember",
171 "--name",
172 "mem",
173 "--type",
174 "project",
175 "--description",
176 "desc",
177 "--format",
178 "text",
179 ])
180 .is_err());
181
182 assert!(
183 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
184 .is_ok()
185 );
186 assert!(
187 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
188 .is_err()
189 );
190
191 assert!(Cli::try_parse_from([
192 "sqlite-graphrag",
193 "rename",
194 "--name",
195 "old",
196 "--new-name",
197 "new",
198 "--format",
199 "json",
200 ])
201 .is_ok());
202 assert!(Cli::try_parse_from([
203 "sqlite-graphrag",
204 "rename",
205 "--name",
206 "old",
207 "--new-name",
208 "new",
209 "--format",
210 "markdown",
211 ])
212 .is_err());
213
214 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
215 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
216 }
217}
218
219impl Cli {
220 pub fn validate_flags(&self) -> Result<(), String> {
225 if let Some(n) = self.max_concurrency {
226 if n == 0 {
227 return Err(match current() {
228 Language::English => "--max-concurrency must be >= 1".to_string(),
229 Language::Portugues => "--max-concurrency deve ser >= 1".to_string(),
230 });
231 }
232 let teto = max_concurrency_ceiling();
233 if n > teto {
234 return Err(match current() {
235 Language::English => format!(
236 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
237 ),
238 Language::Portugues => format!(
239 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
240 ),
241 });
242 }
243 }
244 Ok(())
245 }
246}
247
248impl Commands {
249 pub fn is_embedding_heavy(&self) -> bool {
251 matches!(
252 self,
253 Self::Init(_) | Self::Remember(_) | Self::Recall(_) | Self::HybridSearch(_)
254 )
255 }
256
257 pub fn uses_cli_slot(&self) -> bool {
258 !matches!(self, Self::Daemon(_))
259 }
260}
261
262#[derive(Subcommand)]
263pub enum Commands {
264 Init(init::InitArgs),
266 Daemon(daemon::DaemonArgs),
268 Remember(remember::RememberArgs),
270 Recall(recall::RecallArgs),
272 Read(read::ReadArgs),
274 List(list::ListArgs),
276 Forget(forget::ForgetArgs),
278 Purge(purge::PurgeArgs),
280 Rename(rename::RenameArgs),
282 Edit(edit::EditArgs),
284 History(history::HistoryArgs),
286 Restore(restore::RestoreArgs),
288 HybridSearch(hybrid_search::HybridSearchArgs),
290 Health(health::HealthArgs),
292 Migrate(migrate::MigrateArgs),
294 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
296 Optimize(optimize::OptimizeArgs),
298 Stats(stats::StatsArgs),
300 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
302 Vacuum(vacuum::VacuumArgs),
304 Link(link::LinkArgs),
306 Unlink(unlink::UnlinkArgs),
308 Related(related::RelatedArgs),
310 Graph(graph_export::GraphArgs),
312 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
314 #[command(name = "__debug_schema", hide = true)]
315 DebugSchema(debug_schema::DebugSchemaArgs),
316}
317
318#[derive(Copy, Clone, Debug, clap::ValueEnum)]
319pub enum MemoryType {
320 User,
321 Feedback,
322 Project,
323 Reference,
324 Decision,
325 Incident,
326 Skill,
327}
328
329#[cfg(test)]
330mod testes_concorrencia_pesada {
331 use super::*;
332
333 #[test]
334 fn command_heavy_detecta_init_e_embeddings() {
335 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
336 assert!(init.command.is_embedding_heavy());
337
338 let remember = Cli::try_parse_from([
339 "sqlite-graphrag",
340 "remember",
341 "--name",
342 "memoria-teste",
343 "--type",
344 "project",
345 "--description",
346 "desc",
347 ])
348 .expect("parse remember");
349 assert!(remember.command.is_embedding_heavy());
350
351 let recall =
352 Cli::try_parse_from(["sqlite-graphrag", "recall", "consulta"]).expect("parse recall");
353 assert!(recall.command.is_embedding_heavy());
354
355 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "consulta"])
356 .expect("parse hybrid");
357 assert!(hybrid.command.is_embedding_heavy());
358 }
359
360 #[test]
361 fn command_light_nao_marca_stats() {
362 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
363 assert!(!stats.command.is_embedding_heavy());
364 }
365}
366
367impl MemoryType {
368 pub fn as_str(&self) -> &'static str {
369 match self {
370 Self::User => "user",
371 Self::Feedback => "feedback",
372 Self::Project => "project",
373 Self::Reference => "reference",
374 Self::Decision => "decision",
375 Self::Incident => "incident",
376 Self::Skill => "skill",
377 }
378 }
379}