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 )]
377 pub openrouter_api_key: Option<String>,
378
379 #[command(subcommand)]
380 pub command: Option<Commands>,
381}
382
383#[cfg(test)]
384mod json_only_format_tests {
385 use super::Cli;
386 use clap::Parser;
387
388 #[test]
389 fn restore_accepts_only_format_json() {
390 assert!(Cli::try_parse_from([
391 "sqlite-graphrag",
392 "restore",
393 "--name",
394 "mem",
395 "--version",
396 "1",
397 "--format",
398 "json",
399 ])
400 .is_ok());
401
402 assert!(Cli::try_parse_from([
403 "sqlite-graphrag",
404 "restore",
405 "--name",
406 "mem",
407 "--version",
408 "1",
409 "--format",
410 "text",
411 ])
412 .is_err());
413 }
414
415 #[test]
416 fn hybrid_search_accepts_only_format_json() {
417 assert!(Cli::try_parse_from([
418 "sqlite-graphrag",
419 "hybrid-search",
420 "query",
421 "--format",
422 "json",
423 ])
424 .is_ok());
425
426 assert!(Cli::try_parse_from([
427 "sqlite-graphrag",
428 "hybrid-search",
429 "query",
430 "--format",
431 "markdown",
432 ])
433 .is_err());
434 }
435
436 #[test]
437 fn remember_recall_rename_vacuum_json_only() {
438 assert!(Cli::try_parse_from([
439 "sqlite-graphrag",
440 "remember",
441 "--name",
442 "mem",
443 "--type",
444 "project",
445 "--description",
446 "desc",
447 "--format",
448 "json",
449 ])
450 .is_ok());
451 assert!(Cli::try_parse_from([
452 "sqlite-graphrag",
453 "remember",
454 "--name",
455 "mem",
456 "--type",
457 "project",
458 "--description",
459 "desc",
460 "--format",
461 "text",
462 ])
463 .is_err());
464
465 assert!(
466 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
467 .is_ok()
468 );
469 assert!(
470 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
471 .is_err()
472 );
473
474 assert!(Cli::try_parse_from([
475 "sqlite-graphrag",
476 "rename",
477 "--name",
478 "old",
479 "--new-name",
480 "new",
481 "--format",
482 "json",
483 ])
484 .is_ok());
485 assert!(Cli::try_parse_from([
486 "sqlite-graphrag",
487 "rename",
488 "--name",
489 "old",
490 "--new-name",
491 "new",
492 "--format",
493 "markdown",
494 ])
495 .is_err());
496
497 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
498 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
499 }
500}
501
502impl Cli {
503 pub fn validate_flags(&self) -> Result<(), String> {
508 if let Some(n) = self.max_concurrency {
509 if n == 0 {
510 return Err(match current() {
511 Language::English => "--max-concurrency must be >= 1".to_string(),
512 Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
513 });
514 }
515 let teto = max_concurrency_ceiling();
516 if n > teto {
517 return Err(match current() {
518 Language::English => format!(
519 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
520 ),
521 Language::Portuguese => format!(
522 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
523 ),
524 });
525 }
526 }
527 Ok(())
528 }
529}
530
531impl Commands {
532 pub fn is_embedding_heavy(&self) -> bool {
534 matches!(
535 self,
536 Self::Init(_)
537 | Self::Remember(_)
538 | Self::RememberBatch(_)
539 | Self::Recall(_)
540 | Self::HybridSearch(_)
541 | Self::DeepResearch(_)
542 )
543 }
544
545 pub fn uses_cli_slot(&self) -> bool {
546 true
547 }
548
549 pub fn tolerates_missing_embedding_key(&self) -> bool {
556 match self {
557 Self::Init(_) => true,
558 Self::Enrich(args) => {
559 args.status
560 || args.list_dead
561 || args.requeue_dead
562 || args.prune_dead_orphans
563 || args.prune_dead_entity_orphans
564 }
565 _ => false,
566 }
567 }
568}
569
570#[derive(Debug, clap::Args)]
575pub struct CodexModelsArgs {
576 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
578 pub json: bool,
579}
580
581#[derive(Subcommand)]
582pub enum Commands {
583 #[command(after_long_help = "EXAMPLES:\n \
585 # Initialize in current directory (default behavior)\n \
586 sqlite-graphrag init\n\n \
587 # Initialize at a specific path\n \
588 sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n \
589 # Initialize using SQLITE_GRAPHRAG_HOME env var\n \
590 SQLITE_GRAPHRAG_HOME=/data sqlite-graphrag init\n\n\
591 NOTES:\n \
592 - `init` is OPTIONAL: any subsequent CRUD command auto-initializes graphrag.sqlite if missing.\n \
593 - As a side effect, `init` warms a smoke-test embedding via the LLM-only one-shot pipeline.")]
594 Init(init::InitArgs),
595 #[command(after_long_help = "EXAMPLES:\n \
597 # Inline body\n \
598 sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n \
599 # Body from file\n \
600 sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n \
601 # Body from stdin (pipe)\n \
602 cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n \
603 # Enable automatic URL extraction (URL-regex only since v1.0.79)\n \
604 sqlite-graphrag remember --name rich --type note --description \"...\" --body \"...\" --enable-ner")]
605 Remember(remember::RememberArgs),
606 #[command(after_long_help = "EXAMPLES:\n \
608 # Batch create from NDJSON\n \
609 cat memories.ndjson | sqlite-graphrag remember-batch --force-merge --json\n\n \
610 # Atomic batch\n \
611 cat memories.ndjson | sqlite-graphrag remember-batch --transaction --json")]
612 RememberBatch(remember_batch::RememberBatchArgs),
613 Ingest(ingest::IngestArgs),
615 #[command(after_long_help = "EXAMPLES:\n \
617 # Top 10 semantic matches (default)\n \
618 sqlite-graphrag recall \"agent memory\"\n\n \
619 # Top 3 only\n \
620 sqlite-graphrag recall \"agent memory\" -k 3\n\n \
621 # Search across all namespaces\n \
622 sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n \
623 # Disable graph traversal (vector-only)\n \
624 sqlite-graphrag recall \"agent memory\" --no-graph")]
625 Recall(recall::RecallArgs),
626 Read(read::ReadArgs),
628 List(list::ListArgs),
630 Forget(forget::ForgetArgs),
632 Purge(purge::PurgeArgs),
634 Rename(rename::RenameArgs),
636 SplitBody(split_body::SplitBodyArgs),
638 Edit(edit::EditArgs),
640 History(history::HistoryArgs),
642 Restore(restore::RestoreArgs),
644 #[command(after_long_help = "EXAMPLES:\n \
646 # Hybrid search combining KNN + FTS5 BM25 with RRF\n \
647 sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n \
648 # Custom weights for vector vs full-text components\n \
649 sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
650 HybridSearch(hybrid_search::HybridSearchArgs),
651 Health(health::HealthArgs),
653 Migrate(migrate::MigrateArgs),
655 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
657 Optimize(optimize::OptimizeArgs),
659 Stats(stats::StatsArgs),
661 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
663 Backup(backup::BackupArgs),
665 Vacuum(vacuum::VacuumArgs),
667 Link(link::LinkArgs),
669 Unlink(unlink::UnlinkArgs),
671 #[command(name = "deep-research")]
673 DeepResearch(deep_research::DeepResearchArgs),
674 Related(related::RelatedArgs),
676 Graph(graph_export::GraphArgs),
678 Export(export::ExportArgs),
680 Fts(fts::FtsArgs),
682 Vec(vec::VecArgs),
684 #[command(name = "codex-models")]
690 CodexModels(CodexModelsArgs),
691 PruneRelations(prune_relations::PruneRelationsArgs),
693 #[command(name = "prune-ner")]
695 PruneNer(prune_ner::PruneNerArgs),
696 Slots(slots::SlotsArgs),
698 Pending(pending::PendingArgs),
700 Embedding(embedding::EmbeddingArgs),
702 #[command(name = "pending-embeddings")]
704 PendingEmbeddings(pending_embeddings::PendingEmbeddingsArgs),
705 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
707 MemoryEntities(memory_entities::MemoryEntitiesArgs),
709 Cache(cache::CacheArgs),
711 #[command(name = "delete-entity")]
713 DeleteEntity(delete_entity::DeleteEntityArgs),
714 Reclassify(reclassify::ReclassifyArgs),
716 #[command(name = "rename-entity")]
718 RenameEntity(rename_entity::RenameEntityArgs),
719 #[command(name = "merge-entities")]
721 MergeEntities(merge_entities::MergeEntitiesArgs),
722 Enrich(enrich::EnrichArgs),
724 #[command(name = "reclassify-relation")]
726 ReclassifyRelation(reclassify_relation::ReclassifyRelationArgs),
727 #[command(name = "normalize-entities")]
729 NormalizeEntities(normalize_entities::NormalizeEntitiesArgs),
730 Completions(completions::CompletionsArgs),
732 #[command(name = "debug-schema", hide = true)]
733 DebugSchema(debug_schema::DebugSchemaArgs),
734 Config(config_cmd::ConfigArgs),
736}
737impl std::fmt::Debug for Commands {
743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744 let name = match self {
745 Self::Init(_) => "Init",
746 Self::Health(_) => "Health",
747 Self::Stats(_) => "Stats",
748 Self::List(_) => "List",
749 Self::Read(_) => "Read",
750 Self::Edit(_) => "Edit",
751 Self::Rename(_) => "Rename",
752 Self::SplitBody(_) => "SplitBody",
753 Self::Restore(_) => "Restore",
754 Self::History(_) => "History",
755 Self::Forget(_) => "Forget",
756 Self::Purge(_) => "Purge",
757 Self::Remember(_) => "Remember",
758 Self::RememberBatch(_) => "RememberBatch",
759 Self::Recall(_) => "Recall",
760 Self::HybridSearch(_) => "HybridSearch",
761 Self::Enrich(_) => "Enrich",
762 Self::Ingest(_) => "Ingest",
763 Self::Optimize(_) => "Optimize",
764 Self::Migrate(_) => "Migrate",
765 Self::SyncSafeCopy(_) => "SyncSafeCopy",
766 Self::Backup(_) => "Backup",
767 Self::Vacuum(_) => "Vacuum",
768 Self::Link(_) => "Link",
769 Self::Unlink(_) => "Unlink",
770 Self::DeepResearch(_) => "DeepResearch",
771 Self::Related(_) => "Related",
772 Self::Graph(_) => "Graph",
773 Self::Export(_) => "Export",
774 Self::Fts(_) => "Fts",
775 Self::Vec(_) => "Vec",
776 Self::CodexModels(_) => "CodexModels",
777 Self::PruneRelations(_) => "PruneRelations",
778 Self::PruneNer(_) => "PruneNer",
779 Self::Slots(_) => "Slots",
780 Self::Pending(_) => "Pending",
781 Self::Embedding(_) => "Embedding",
782 Self::PendingEmbeddings(_) => "PendingEmbeddings",
783 Self::CleanupOrphans(_) => "CleanupOrphans",
784 Self::MemoryEntities(_) => "MemoryEntities",
785 Self::Cache(_) => "Cache",
786 Self::DeleteEntity(_) => "DeleteEntity",
787 Self::Reclassify(_) => "Reclassify",
788 Self::RenameEntity(_) => "RenameEntity",
789 Self::ReclassifyRelation(_) => "ReclassifyRelation",
790 Self::NormalizeEntities(_) => "NormalizeEntities",
791 Self::MergeEntities(_) => "MergeEntities",
792 Self::NamespaceDetect(_) => "NamespaceDetect",
793 Self::Completions(_) => "Completions",
794 Self::DebugSchema(_) => "DebugSchema",
795 Self::Config(_) => "Config",
796 };
797 f.write_str(name)
798 }
799}
800
801#[derive(Copy, Clone, Debug, Default, clap::ValueEnum)]
802pub enum MemoryType {
803 User,
804 Feedback,
805 Project,
806 Reference,
807 Decision,
808 Incident,
809 Skill,
810 #[default]
811 Document,
812 Note,
813}
814
815#[cfg(test)]
816mod heavy_concurrency_tests {
817 use super::*;
818
819 #[test]
820 fn command_heavy_detects_init_and_embeddings() {
821 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
822 assert!(init
823 .command
824 .as_ref()
825 .is_some_and(|c| c.is_embedding_heavy()));
826
827 let remember = Cli::try_parse_from([
828 "sqlite-graphrag",
829 "remember",
830 "--name",
831 "test-memory",
832 "--type",
833 "project",
834 "--description",
835 "desc",
836 ])
837 .expect("parse remember");
838 assert!(remember
839 .command
840 .as_ref()
841 .is_some_and(|c| c.is_embedding_heavy()));
842
843 let recall =
844 Cli::try_parse_from(["sqlite-graphrag", "recall", "query"]).expect("parse recall");
845 assert!(recall
846 .command
847 .as_ref()
848 .is_some_and(|c| c.is_embedding_heavy()));
849
850 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "query"])
851 .expect("parse hybrid");
852 assert!(hybrid
853 .command
854 .as_ref()
855 .is_some_and(|c| c.is_embedding_heavy()));
856 }
857
858 #[test]
859 fn command_light_does_not_mark_stats() {
860 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
861 assert!(!stats
862 .command
863 .as_ref()
864 .is_some_and(|c| c.is_embedding_heavy()));
865 }
866}
867
868impl MemoryType {
869 pub fn as_str(&self) -> &'static str {
870 match self {
871 Self::User => "user",
872 Self::Feedback => "feedback",
873 Self::Project => "project",
874 Self::Reference => "reference",
875 Self::Decision => "decision",
876 Self::Incident => "incident",
877 Self::Skill => "skill",
878 Self::Document => "document",
879 Self::Note => "note",
880 }
881 }
882}
883
884#[cfg(test)]
886mod fase_g_parsing_tests {
887 use super::Cli;
888 use clap::Parser;
889
890 #[test]
892 fn enrich_status_optional_operation_and_mode() {
893 assert!(
894 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--status"]).is_ok(),
895 "--status alone must not require --operation/--mode"
896 );
897 assert!(
898 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--list-dead"]).is_ok(),
899 "--list-dead is read-only and must not require --operation/--mode"
900 );
901 assert!(
903 Cli::try_parse_from(["sqlite-graphrag", "enrich"]).is_err(),
904 "bare enrich (no status/list-dead/requeue-dead) must require --operation/--mode"
905 );
906 assert!(Cli::try_parse_from([
908 "sqlite-graphrag",
909 "enrich",
910 "--operation",
911 "memory-bindings",
912 "--mode",
913 "openrouter",
914 ])
915 .is_ok());
916 }
917
918 #[test]
920 fn config_doctor_accepts_json() {
921 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "doctor", "--json"]).is_ok());
922 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "list-keys", "--json"]).is_ok());
923 }
924
925 #[test]
928 fn remember_description_allows_leading_hyphen() {
929 assert!(Cli::try_parse_from([
930 "sqlite-graphrag",
931 "remember",
932 "--name",
933 "mem",
934 "--type",
935 "note",
936 "--description",
937 "- bullet description",
938 ])
939 .is_ok());
940 }
941
942 #[test]
944 fn remember_batch_accepts_llm_parallelism() {
945 assert!(Cli::try_parse_from([
946 "sqlite-graphrag",
947 "remember-batch",
948 "--llm-parallelism",
949 "4"
950 ])
951 .is_ok());
952 }
953
954 #[test]
957 fn remember_graph_file_combines_with_body_but_conflicts_with_graph_stdin() {
958 assert!(
959 Cli::try_parse_from([
960 "sqlite-graphrag",
961 "remember",
962 "--name",
963 "mem",
964 "--type",
965 "note",
966 "--body",
967 "inline body",
968 "--graph-file",
969 "/tmp/graph.json",
970 ])
971 .is_ok(),
972 "--body + --graph-file must coexist"
973 );
974 assert!(
975 Cli::try_parse_from([
976 "sqlite-graphrag",
977 "remember",
978 "--name",
979 "mem",
980 "--type",
981 "note",
982 "--graph-file",
983 "/tmp/graph.json",
984 "--graph-stdin",
985 ])
986 .is_err(),
987 "--graph-file conflicts with --graph-stdin"
988 );
989 }
990}