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