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