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, PartialEq, Eq, clap::ValueEnum)]
17pub enum GraphExportFormat {
18 Json,
19 Dot,
20 Mermaid,
21 Ndjson,
23}
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
29pub enum LlmBackendChoice {
30 Auto,
31 Claude,
32 Codex,
33 Opencode,
34 OpenRouter,
35 None,
36}
37
38#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
44pub enum EmbeddingBackendChoice {
45 Auto,
46 Openrouter,
47 Llm,
48}
49
50impl EmbeddingBackendChoice {
51 pub fn to_chain(self, llm_choice: LlmBackendChoice) -> Vec<crate::embedder::LlmBackendKind> {
54 use crate::embedder::LlmBackendKind;
55 match self {
56 EmbeddingBackendChoice::Openrouter => vec![LlmBackendKind::OpenRouter],
57 EmbeddingBackendChoice::Llm => llm_choice.to_chain(),
58 EmbeddingBackendChoice::Auto => {
59 if crate::embedder::is_openrouter_initialized() {
60 let mut chain = vec![LlmBackendKind::OpenRouter];
61 chain.extend(llm_choice.to_chain());
62 chain
63 } else {
64 llm_choice.to_chain()
65 }
66 }
67 }
68 }
69}
70
71impl LlmBackendChoice {
72 pub fn to_chain(self) -> Vec<crate::embedder::LlmBackendKind> {
82 use crate::embedder::LlmBackendKind;
83 match self {
84 LlmBackendChoice::Codex => vec![LlmBackendKind::Codex, LlmBackendKind::None],
85 LlmBackendChoice::Claude => vec![LlmBackendKind::Claude, LlmBackendKind::None],
86 LlmBackendChoice::Opencode => vec![
87 LlmBackendKind::Opencode,
88 LlmBackendKind::Codex,
89 LlmBackendKind::Claude,
90 LlmBackendKind::None,
91 ],
92 LlmBackendChoice::OpenRouter => vec![
93 LlmBackendKind::OpenRouter,
94 LlmBackendKind::Codex,
95 LlmBackendKind::None,
96 ],
97 LlmBackendChoice::None => vec![LlmBackendKind::None],
98 LlmBackendChoice::Auto => parse_fallback_chain(
99 &std::env::var("SQLITE_GRAPHRAG_LLM_FALLBACK")
100 .unwrap_or_else(|_| "codex,claude,none".to_string()),
101 ),
102 }
103 }
104}
105
106fn parse_fallback_chain(s: &str) -> Vec<crate::embedder::LlmBackendKind> {
107 use crate::embedder::LlmBackendKind;
108 let mut chain: Vec<LlmBackendKind> = s
109 .split(',')
110 .filter_map(|tok| match tok.trim().to_ascii_lowercase().as_str() {
111 "codex" => Some(LlmBackendKind::Codex),
112 "claude" | "claude-code" => Some(LlmBackendKind::Claude),
113 "opencode" => Some(LlmBackendKind::Opencode),
114 "openrouter" => Some(LlmBackendKind::OpenRouter),
115 "none" => Some(LlmBackendKind::None),
116 _ => {
117 tracing::warn!(
118 token = tok.trim(),
119 "unknown backend in --llm-fallback, skipping"
120 );
121 Option::None
122 }
123 })
124 .collect();
125 if chain.is_empty() {
126 chain = vec![
127 LlmBackendKind::Codex,
128 LlmBackendKind::Claude,
129 LlmBackendKind::None,
130 ];
131 }
132 chain
133}
134
135#[derive(Parser)]
136#[command(name = "sqlite-graphrag")]
137#[command(version)]
138#[command(about = "Local GraphRAG memory for LLMs in a single SQLite file")]
139#[command(arg_required_else_help = true)]
140#[command(after_help = "DATABASE PATH (GAP-SG-32):\n \
141 `--db` is a PER-SUBCOMMAND flag, so it must come AFTER the subcommand:\n \
142 sqlite-graphrag remember --db ./graphrag.sqlite --name mem --type note ...\n \
143 Placing it before the subcommand (e.g. `sqlite-graphrag --db x.sqlite remember`) is rejected.\n \
144 For a position-independent path, set the canonical env var instead:\n \
145 SQLITE_GRAPHRAG_DB_PATH=./graphrag.sqlite sqlite-graphrag remember --name mem ...")]
146pub struct Cli {
147 #[arg(long, global = true, value_name = "N")]
152 pub max_concurrency: Option<usize>,
153
154 #[arg(long, global = true, value_name = "SECONDS")]
159 pub wait_lock: Option<u64>,
160
161 #[arg(long, global = true, hide = true, default_value_t = false)]
165 pub skip_memory_guard: bool,
166
167 #[arg(
176 long,
177 global = true,
178 hide = true,
179 default_value_t = false,
180 value_parser = clap::builder::BoolishValueParser::new(),
181 env = "SQLITE_GRAPHRAG_STRICT_ENV_CLEAR"
182 )]
183 pub strict_env_clear: bool,
184
185 #[arg(
192 long,
193 global = true,
194 hide = true,
195 default_value_t = false,
196 value_parser = clap::builder::BoolishValueParser::new(),
197 env = "SQLITE_GRAPHRAG_DRY_RUN_BACKEND"
198 )]
199 pub dry_run_backend: bool,
200
201 #[arg(long, global = true, value_enum, value_name = "LANG")]
207 pub lang: Option<crate::i18n::Language>,
208
209 #[arg(long, global = true, value_name = "IANA")]
215 pub tz: Option<chrono_tz::Tz>,
216
217 #[arg(short = 'v', long, global = true, action = clap::ArgAction::Count)]
222 pub verbose: u8,
223
224 #[arg(long, global = true, value_name = "KIND", default_value = "llm")]
231 pub extraction_backend: Option<String>,
232
233 #[arg(long, global = true, value_name = "N", value_parser = clap::value_parser!(u64).range(8..=4096))]
241 pub embedding_dim: Option<u64>,
242
243 #[arg(long, global = true, value_enum, default_value_t = LlmBackendChoice::Auto, env = "SQLITE_GRAPHRAG_LLM_BACKEND")]
251 pub llm_backend: LlmBackendChoice,
252
253 #[arg(
257 long,
258 global = true,
259 value_name = "MODEL",
260 env = "SQLITE_GRAPHRAG_LLM_MODEL"
261 )]
262 pub llm_model: Option<String>,
263
264 #[arg(
267 long,
268 global = true,
269 value_name = "PATH",
270 env = "SQLITE_GRAPHRAG_CLAUDE_BINARY"
271 )]
272 pub claude_binary: Option<std::path::PathBuf>,
273
274 #[arg(
277 long,
278 global = true,
279 value_name = "PATH",
280 env = "SQLITE_GRAPHRAG_CODEX_BINARY"
281 )]
282 pub codex_binary: Option<std::path::PathBuf>,
283
284 #[arg(
287 long,
288 global = true,
289 value_name = "PATH",
290 env = "SQLITE_GRAPHRAG_OPENCODE_BINARY"
291 )]
292 pub opencode_binary: Option<std::path::PathBuf>,
293
294 #[arg(
298 long,
299 global = true,
300 default_value = "codex,claude,none",
301 env = "SQLITE_GRAPHRAG_LLM_FALLBACK"
302 )]
303 pub llm_fallback: String,
304
305 #[arg(
310 long,
311 global = true,
312 default_value_t = false,
313 value_parser = clap::builder::BoolishValueParser::new(),
314 env = "SQLITE_GRAPHRAG_SKIP_EMBEDDING_ON_FAILURE"
315 )]
316 pub skip_embedding_on_failure: bool,
317
318 #[arg(
322 long,
323 global = true,
324 value_name = "N",
325 env = "SQLITE_GRAPHRAG_LLM_MAX_HOST_CONCURRENCY"
326 )]
327 pub llm_max_host_concurrency: Option<u32>,
328
329 #[arg(
333 long,
334 global = true,
335 value_name = "SECONDS",
336 env = "SQLITE_GRAPHRAG_LLM_SLOT_WAIT_SECS"
337 )]
338 pub llm_slot_wait_secs: Option<u64>,
339
340 #[arg(
344 long,
345 global = true,
346 default_value_t = false,
347 value_parser = clap::builder::BoolishValueParser::new(),
348 env = "SQLITE_GRAPHRAG_LLM_SLOT_NO_WAIT"
349 )]
350 pub llm_slot_no_wait: bool,
351
352 #[arg(long, global = true, value_enum, default_value_t = EmbeddingBackendChoice::Auto, env = "SQLITE_GRAPHRAG_EMBEDDING_BACKEND")]
356 pub embedding_backend: EmbeddingBackendChoice,
357
358 #[arg(
361 long,
362 global = true,
363 value_name = "MODEL",
364 env = "SQLITE_GRAPHRAG_EMBEDDING_MODEL"
365 )]
366 pub embedding_model: Option<String>,
367
368 #[arg(
371 long,
372 global = true,
373 value_name = "KEY",
374 hide = true,
375 env = "OPENROUTER_API_KEY",
376 hide_env_values = true
377 )]
378 pub openrouter_api_key: Option<String>,
379
380 #[command(subcommand)]
381 pub command: Option<Commands>,
382}
383
384#[cfg(test)]
385mod json_only_format_tests {
386 use super::Cli;
387 use clap::Parser;
388
389 #[test]
390 fn restore_accepts_only_format_json() {
391 assert!(Cli::try_parse_from([
392 "sqlite-graphrag",
393 "restore",
394 "--name",
395 "mem",
396 "--version",
397 "1",
398 "--format",
399 "json",
400 ])
401 .is_ok());
402
403 assert!(Cli::try_parse_from([
404 "sqlite-graphrag",
405 "restore",
406 "--name",
407 "mem",
408 "--version",
409 "1",
410 "--format",
411 "text",
412 ])
413 .is_err());
414 }
415
416 #[test]
417 fn hybrid_search_accepts_only_format_json() {
418 assert!(Cli::try_parse_from([
419 "sqlite-graphrag",
420 "hybrid-search",
421 "query",
422 "--format",
423 "json",
424 ])
425 .is_ok());
426
427 assert!(Cli::try_parse_from([
428 "sqlite-graphrag",
429 "hybrid-search",
430 "query",
431 "--format",
432 "markdown",
433 ])
434 .is_err());
435 }
436
437 #[test]
438 fn remember_recall_rename_vacuum_json_only() {
439 assert!(Cli::try_parse_from([
440 "sqlite-graphrag",
441 "remember",
442 "--name",
443 "mem",
444 "--type",
445 "project",
446 "--description",
447 "desc",
448 "--format",
449 "json",
450 ])
451 .is_ok());
452 assert!(Cli::try_parse_from([
453 "sqlite-graphrag",
454 "remember",
455 "--name",
456 "mem",
457 "--type",
458 "project",
459 "--description",
460 "desc",
461 "--format",
462 "text",
463 ])
464 .is_err());
465
466 assert!(
467 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
468 .is_ok()
469 );
470 assert!(
471 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
472 .is_err()
473 );
474
475 assert!(Cli::try_parse_from([
476 "sqlite-graphrag",
477 "rename",
478 "--name",
479 "old",
480 "--new-name",
481 "new",
482 "--format",
483 "json",
484 ])
485 .is_ok());
486 assert!(Cli::try_parse_from([
487 "sqlite-graphrag",
488 "rename",
489 "--name",
490 "old",
491 "--new-name",
492 "new",
493 "--format",
494 "markdown",
495 ])
496 .is_err());
497
498 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
499 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
500 }
501}
502
503impl Cli {
504 pub fn validate_flags(&self) -> Result<(), String> {
509 if let Some(n) = self.max_concurrency {
510 if n == 0 {
511 return Err(match current() {
512 Language::English => "--max-concurrency must be >= 1".to_string(),
513 Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
514 });
515 }
516 let teto = max_concurrency_ceiling();
517 if n > teto {
518 return Err(match current() {
519 Language::English => format!(
520 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
521 ),
522 Language::Portuguese => format!(
523 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
524 ),
525 });
526 }
527 }
528 Ok(())
529 }
530}
531
532impl Commands {
533 pub fn is_embedding_heavy(&self) -> bool {
535 matches!(
536 self,
537 Self::Init(_)
538 | Self::Remember(_)
539 | Self::RememberBatch(_)
540 | Self::Recall(_)
541 | Self::HybridSearch(_)
542 | Self::DeepResearch(_)
543 )
544 }
545
546 pub fn uses_cli_slot(&self) -> bool {
547 true
548 }
549
550 pub fn tolerates_missing_embedding_key(&self) -> bool {
557 match self {
558 Self::Init(_) => true,
559 Self::Enrich(args) => {
560 args.status
561 || args.list_dead
562 || args.requeue_dead
563 || args.prune_dead_orphans
564 || args.prune_dead_entity_orphans
565 }
566 _ => false,
567 }
568 }
569}
570
571#[derive(Debug, clap::Args)]
576pub struct CodexModelsArgs {
577 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
579 pub json: bool,
580}
581
582#[derive(Subcommand)]
583pub enum Commands {
584 #[command(after_long_help = "EXAMPLES:\n \
586 # Initialize in current directory (default behavior)\n \
587 sqlite-graphrag init\n\n \
588 # Initialize at a specific path\n \
589 sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n \
590 # Initialize using SQLITE_GRAPHRAG_HOME env var\n \
591 SQLITE_GRAPHRAG_HOME=/data sqlite-graphrag init\n\n\
592 NOTES:\n \
593 - `init` is OPTIONAL: any subsequent CRUD command auto-initializes graphrag.sqlite if missing.\n \
594 - As a side effect, `init` warms a smoke-test embedding via the LLM-only one-shot pipeline.")]
595 Init(init::InitArgs),
596 #[command(after_long_help = "EXAMPLES:\n \
598 # Inline body\n \
599 sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n \
600 # Body from file\n \
601 sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n \
602 # Body from stdin (pipe)\n \
603 cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n \
604 # Enable automatic URL extraction (URL-regex only since v1.0.79)\n \
605 sqlite-graphrag remember --name rich --type note --description \"...\" --body \"...\" --enable-ner")]
606 Remember(remember::RememberArgs),
607 #[command(after_long_help = "EXAMPLES:\n \
609 # Batch create from NDJSON\n \
610 cat memories.ndjson | sqlite-graphrag remember-batch --force-merge --json\n\n \
611 # Atomic batch\n \
612 cat memories.ndjson | sqlite-graphrag remember-batch --transaction --json")]
613 RememberBatch(remember_batch::RememberBatchArgs),
614 Ingest(ingest::IngestArgs),
616 #[command(after_long_help = "EXAMPLES:\n \
618 # Top 10 semantic matches (default)\n \
619 sqlite-graphrag recall \"agent memory\"\n\n \
620 # Top 3 only\n \
621 sqlite-graphrag recall \"agent memory\" -k 3\n\n \
622 # Search across all namespaces\n \
623 sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n \
624 # Disable graph traversal (vector-only)\n \
625 sqlite-graphrag recall \"agent memory\" --no-graph")]
626 Recall(recall::RecallArgs),
627 Read(read::ReadArgs),
629 List(list::ListArgs),
631 Forget(forget::ForgetArgs),
633 Purge(purge::PurgeArgs),
635 Rename(rename::RenameArgs),
637 SplitBody(split_body::SplitBodyArgs),
639 Edit(edit::EditArgs),
641 History(history::HistoryArgs),
643 Restore(restore::RestoreArgs),
645 #[command(after_long_help = "EXAMPLES:\n \
647 # Hybrid search combining KNN + FTS5 BM25 with RRF\n \
648 sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n \
649 # Custom weights for vector vs full-text components\n \
650 sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
651 HybridSearch(hybrid_search::HybridSearchArgs),
652 Health(health::HealthArgs),
654 Migrate(migrate::MigrateArgs),
656 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
658 Optimize(optimize::OptimizeArgs),
660 Stats(stats::StatsArgs),
662 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
664 Backup(backup::BackupArgs),
666 Vacuum(vacuum::VacuumArgs),
668 Link(link::LinkArgs),
670 Unlink(unlink::UnlinkArgs),
672 #[command(name = "deep-research")]
674 DeepResearch(deep_research::DeepResearchArgs),
675 Related(related::RelatedArgs),
677 Graph(graph_export::GraphArgs),
679 Export(export::ExportArgs),
681 Fts(fts::FtsArgs),
683 Vec(vec::VecArgs),
685 #[command(name = "codex-models")]
691 CodexModels(CodexModelsArgs),
692 PruneRelations(prune_relations::PruneRelationsArgs),
694 #[command(name = "prune-ner")]
696 PruneNer(prune_ner::PruneNerArgs),
697 Slots(slots::SlotsArgs),
699 Pending(pending::PendingArgs),
701 Embedding(embedding::EmbeddingArgs),
703 #[command(name = "pending-embeddings")]
705 PendingEmbeddings(pending_embeddings::PendingEmbeddingsArgs),
706 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
708 MemoryEntities(memory_entities::MemoryEntitiesArgs),
710 Cache(cache::CacheArgs),
712 #[command(name = "delete-entity")]
714 DeleteEntity(delete_entity::DeleteEntityArgs),
715 Reclassify(reclassify::ReclassifyArgs),
717 #[command(name = "rename-entity")]
719 RenameEntity(rename_entity::RenameEntityArgs),
720 #[command(name = "merge-entities")]
722 MergeEntities(merge_entities::MergeEntitiesArgs),
723 Enrich(enrich::EnrichArgs),
725 #[command(name = "reclassify-relation")]
727 ReclassifyRelation(reclassify_relation::ReclassifyRelationArgs),
728 #[command(name = "normalize-entities")]
730 NormalizeEntities(normalize_entities::NormalizeEntitiesArgs),
731 Completions(completions::CompletionsArgs),
733 #[command(name = "debug-schema", hide = true)]
734 DebugSchema(debug_schema::DebugSchemaArgs),
735 Config(config_cmd::ConfigArgs),
737}
738impl std::fmt::Debug for Commands {
744 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
745 let name = match self {
746 Self::Init(_) => "Init",
747 Self::Health(_) => "Health",
748 Self::Stats(_) => "Stats",
749 Self::List(_) => "List",
750 Self::Read(_) => "Read",
751 Self::Edit(_) => "Edit",
752 Self::Rename(_) => "Rename",
753 Self::SplitBody(_) => "SplitBody",
754 Self::Restore(_) => "Restore",
755 Self::History(_) => "History",
756 Self::Forget(_) => "Forget",
757 Self::Purge(_) => "Purge",
758 Self::Remember(_) => "Remember",
759 Self::RememberBatch(_) => "RememberBatch",
760 Self::Recall(_) => "Recall",
761 Self::HybridSearch(_) => "HybridSearch",
762 Self::Enrich(_) => "Enrich",
763 Self::Ingest(_) => "Ingest",
764 Self::Optimize(_) => "Optimize",
765 Self::Migrate(_) => "Migrate",
766 Self::SyncSafeCopy(_) => "SyncSafeCopy",
767 Self::Backup(_) => "Backup",
768 Self::Vacuum(_) => "Vacuum",
769 Self::Link(_) => "Link",
770 Self::Unlink(_) => "Unlink",
771 Self::DeepResearch(_) => "DeepResearch",
772 Self::Related(_) => "Related",
773 Self::Graph(_) => "Graph",
774 Self::Export(_) => "Export",
775 Self::Fts(_) => "Fts",
776 Self::Vec(_) => "Vec",
777 Self::CodexModels(_) => "CodexModels",
778 Self::PruneRelations(_) => "PruneRelations",
779 Self::PruneNer(_) => "PruneNer",
780 Self::Slots(_) => "Slots",
781 Self::Pending(_) => "Pending",
782 Self::Embedding(_) => "Embedding",
783 Self::PendingEmbeddings(_) => "PendingEmbeddings",
784 Self::CleanupOrphans(_) => "CleanupOrphans",
785 Self::MemoryEntities(_) => "MemoryEntities",
786 Self::Cache(_) => "Cache",
787 Self::DeleteEntity(_) => "DeleteEntity",
788 Self::Reclassify(_) => "Reclassify",
789 Self::RenameEntity(_) => "RenameEntity",
790 Self::ReclassifyRelation(_) => "ReclassifyRelation",
791 Self::NormalizeEntities(_) => "NormalizeEntities",
792 Self::MergeEntities(_) => "MergeEntities",
793 Self::NamespaceDetect(_) => "NamespaceDetect",
794 Self::Completions(_) => "Completions",
795 Self::DebugSchema(_) => "DebugSchema",
796 Self::Config(_) => "Config",
797 };
798 f.write_str(name)
799 }
800}
801
802#[derive(Copy, Clone, Debug, Default, clap::ValueEnum)]
803pub enum MemoryType {
804 User,
805 Feedback,
806 Project,
807 Reference,
808 Decision,
809 Incident,
810 Skill,
811 #[default]
812 Document,
813 Note,
814}
815
816#[cfg(test)]
817mod heavy_concurrency_tests {
818 use super::*;
819
820 #[test]
821 fn command_heavy_detects_init_and_embeddings() {
822 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
823 assert!(init
824 .command
825 .as_ref()
826 .is_some_and(|c| c.is_embedding_heavy()));
827
828 let remember = Cli::try_parse_from([
829 "sqlite-graphrag",
830 "remember",
831 "--name",
832 "test-memory",
833 "--type",
834 "project",
835 "--description",
836 "desc",
837 ])
838 .expect("parse remember");
839 assert!(remember
840 .command
841 .as_ref()
842 .is_some_and(|c| c.is_embedding_heavy()));
843
844 let recall =
845 Cli::try_parse_from(["sqlite-graphrag", "recall", "query"]).expect("parse recall");
846 assert!(recall
847 .command
848 .as_ref()
849 .is_some_and(|c| c.is_embedding_heavy()));
850
851 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "query"])
852 .expect("parse hybrid");
853 assert!(hybrid
854 .command
855 .as_ref()
856 .is_some_and(|c| c.is_embedding_heavy()));
857 }
858
859 #[test]
860 fn command_light_does_not_mark_stats() {
861 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
862 assert!(!stats
863 .command
864 .as_ref()
865 .is_some_and(|c| c.is_embedding_heavy()));
866 }
867}
868
869impl MemoryType {
870 pub fn as_str(&self) -> &'static str {
871 match self {
872 Self::User => "user",
873 Self::Feedback => "feedback",
874 Self::Project => "project",
875 Self::Reference => "reference",
876 Self::Decision => "decision",
877 Self::Incident => "incident",
878 Self::Skill => "skill",
879 Self::Document => "document",
880 Self::Note => "note",
881 }
882 }
883}
884
885#[cfg(test)]
887mod fase_g_parsing_tests {
888 use super::Cli;
889 use clap::Parser;
890
891 #[test]
893 fn enrich_status_optional_operation_and_mode() {
894 assert!(
895 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--status"]).is_ok(),
896 "--status alone must not require --operation/--mode"
897 );
898 assert!(
899 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--list-dead"]).is_ok(),
900 "--list-dead is read-only and must not require --operation/--mode"
901 );
902 assert!(
904 Cli::try_parse_from(["sqlite-graphrag", "enrich"]).is_err(),
905 "bare enrich (no status/list-dead/requeue-dead) must require --operation/--mode"
906 );
907 assert!(Cli::try_parse_from([
909 "sqlite-graphrag",
910 "enrich",
911 "--operation",
912 "memory-bindings",
913 "--mode",
914 "openrouter",
915 ])
916 .is_ok());
917 }
918
919 #[test]
921 fn config_doctor_accepts_json() {
922 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "doctor", "--json"]).is_ok());
923 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "list-keys", "--json"]).is_ok());
924 }
925
926 #[test]
929 fn remember_description_allows_leading_hyphen() {
930 assert!(Cli::try_parse_from([
931 "sqlite-graphrag",
932 "remember",
933 "--name",
934 "mem",
935 "--type",
936 "note",
937 "--description",
938 "- bullet description",
939 ])
940 .is_ok());
941 }
942
943 #[test]
945 fn remember_batch_accepts_llm_parallelism() {
946 assert!(Cli::try_parse_from([
947 "sqlite-graphrag",
948 "remember-batch",
949 "--llm-parallelism",
950 "4"
951 ])
952 .is_ok());
953 }
954
955 #[test]
958 fn remember_graph_file_combines_with_body_but_conflicts_with_graph_stdin() {
959 assert!(
960 Cli::try_parse_from([
961 "sqlite-graphrag",
962 "remember",
963 "--name",
964 "mem",
965 "--type",
966 "note",
967 "--body",
968 "inline body",
969 "--graph-file",
970 "/tmp/graph.json",
971 ])
972 .is_ok(),
973 "--body + --graph-file must coexist"
974 );
975 assert!(
976 Cli::try_parse_from([
977 "sqlite-graphrag",
978 "remember",
979 "--name",
980 "mem",
981 "--type",
982 "note",
983 "--graph-file",
984 "/tmp/graph.json",
985 "--graph-stdin",
986 ])
987 .is_err(),
988 "--graph-file conflicts with --graph-stdin"
989 );
990 }
991}