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(short = 'q', long, global = true, default_value_t = false)]
231 pub quiet: bool,
232
233 #[arg(long, global = true, value_name = "KIND", default_value = "llm")]
240 pub extraction_backend: Option<String>,
241
242 #[arg(long, global = true, value_name = "N", value_parser = clap::value_parser!(u64).range(8..=4096))]
250 pub embedding_dim: Option<u64>,
251
252 #[arg(long, global = true, value_enum, default_value_t = LlmBackendChoice::Auto, env = "SQLITE_GRAPHRAG_LLM_BACKEND")]
260 pub llm_backend: LlmBackendChoice,
261
262 #[arg(
266 long,
267 global = true,
268 value_name = "MODEL",
269 env = "SQLITE_GRAPHRAG_LLM_MODEL"
270 )]
271 pub llm_model: Option<String>,
272
273 #[arg(
276 long,
277 global = true,
278 value_name = "PATH",
279 env = "SQLITE_GRAPHRAG_CLAUDE_BINARY"
280 )]
281 pub claude_binary: Option<std::path::PathBuf>,
282
283 #[arg(
286 long,
287 global = true,
288 value_name = "PATH",
289 env = "SQLITE_GRAPHRAG_CODEX_BINARY"
290 )]
291 pub codex_binary: Option<std::path::PathBuf>,
292
293 #[arg(
296 long,
297 global = true,
298 value_name = "PATH",
299 env = "SQLITE_GRAPHRAG_OPENCODE_BINARY"
300 )]
301 pub opencode_binary: Option<std::path::PathBuf>,
302
303 #[arg(
307 long,
308 global = true,
309 default_value = "codex,claude,none",
310 env = "SQLITE_GRAPHRAG_LLM_FALLBACK"
311 )]
312 pub llm_fallback: String,
313
314 #[arg(
319 long,
320 global = true,
321 default_value_t = false,
322 value_parser = clap::builder::BoolishValueParser::new(),
323 env = "SQLITE_GRAPHRAG_SKIP_EMBEDDING_ON_FAILURE"
324 )]
325 pub skip_embedding_on_failure: bool,
326
327 #[arg(
331 long,
332 global = true,
333 value_name = "N",
334 env = "SQLITE_GRAPHRAG_LLM_MAX_HOST_CONCURRENCY"
335 )]
336 pub llm_max_host_concurrency: Option<u32>,
337
338 #[arg(
342 long,
343 global = true,
344 value_name = "SECONDS",
345 env = "SQLITE_GRAPHRAG_LLM_SLOT_WAIT_SECS"
346 )]
347 pub llm_slot_wait_secs: Option<u64>,
348
349 #[arg(
353 long,
354 global = true,
355 default_value_t = false,
356 value_parser = clap::builder::BoolishValueParser::new(),
357 env = "SQLITE_GRAPHRAG_LLM_SLOT_NO_WAIT"
358 )]
359 pub llm_slot_no_wait: bool,
360
361 #[arg(long, global = true, value_enum, default_value_t = EmbeddingBackendChoice::Auto, env = "SQLITE_GRAPHRAG_EMBEDDING_BACKEND")]
365 pub embedding_backend: EmbeddingBackendChoice,
366
367 #[arg(
370 long,
371 global = true,
372 value_name = "MODEL",
373 env = "SQLITE_GRAPHRAG_EMBEDDING_MODEL"
374 )]
375 pub embedding_model: Option<String>,
376
377 #[arg(
380 long,
381 global = true,
382 value_name = "KEY",
383 hide = true,
384 env = "OPENROUTER_API_KEY",
385 hide_env_values = true
386 )]
387 pub openrouter_api_key: Option<String>,
388
389 #[command(subcommand)]
390 pub command: Option<Commands>,
391}
392
393#[cfg(test)]
394mod json_only_format_tests {
395 use super::Cli;
396 use clap::Parser;
397
398 #[test]
399 fn restore_accepts_only_format_json() {
400 assert!(Cli::try_parse_from([
401 "sqlite-graphrag",
402 "restore",
403 "--name",
404 "mem",
405 "--version",
406 "1",
407 "--format",
408 "json",
409 ])
410 .is_ok());
411
412 assert!(Cli::try_parse_from([
413 "sqlite-graphrag",
414 "restore",
415 "--name",
416 "mem",
417 "--version",
418 "1",
419 "--format",
420 "text",
421 ])
422 .is_err());
423 }
424
425 #[test]
426 fn hybrid_search_accepts_only_format_json() {
427 assert!(Cli::try_parse_from([
428 "sqlite-graphrag",
429 "hybrid-search",
430 "query",
431 "--format",
432 "json",
433 ])
434 .is_ok());
435
436 assert!(Cli::try_parse_from([
437 "sqlite-graphrag",
438 "hybrid-search",
439 "query",
440 "--format",
441 "markdown",
442 ])
443 .is_err());
444 }
445
446 #[test]
447 fn remember_recall_rename_vacuum_json_only() {
448 assert!(Cli::try_parse_from([
449 "sqlite-graphrag",
450 "remember",
451 "--name",
452 "mem",
453 "--type",
454 "project",
455 "--description",
456 "desc",
457 "--format",
458 "json",
459 ])
460 .is_ok());
461 assert!(Cli::try_parse_from([
462 "sqlite-graphrag",
463 "remember",
464 "--name",
465 "mem",
466 "--type",
467 "project",
468 "--description",
469 "desc",
470 "--format",
471 "text",
472 ])
473 .is_err());
474
475 assert!(
476 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "json",])
477 .is_ok()
478 );
479 assert!(
480 Cli::try_parse_from(["sqlite-graphrag", "recall", "query", "--format", "text",])
481 .is_err()
482 );
483
484 assert!(Cli::try_parse_from([
485 "sqlite-graphrag",
486 "rename",
487 "--name",
488 "old",
489 "--new-name",
490 "new",
491 "--format",
492 "json",
493 ])
494 .is_ok());
495 assert!(Cli::try_parse_from([
496 "sqlite-graphrag",
497 "rename",
498 "--name",
499 "old",
500 "--new-name",
501 "new",
502 "--format",
503 "markdown",
504 ])
505 .is_err());
506
507 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "json",]).is_ok());
508 assert!(Cli::try_parse_from(["sqlite-graphrag", "vacuum", "--format", "text",]).is_err());
509 }
510}
511
512impl Cli {
513 pub fn validate_flags(&self) -> Result<(), String> {
518 if let Some(n) = self.max_concurrency {
519 if n == 0 {
520 return Err(match current() {
521 Language::English => "--max-concurrency must be >= 1".to_string(),
522 Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
523 });
524 }
525 let teto = max_concurrency_ceiling();
526 if n > teto {
527 return Err(match current() {
528 Language::English => format!(
529 "--max-concurrency {n} exceeds the ceiling of {teto} (2×nCPUs) on this system"
530 ),
531 Language::Portuguese => format!(
532 "--max-concurrency {n} excede o teto de {teto} (2×nCPUs) neste sistema"
533 ),
534 });
535 }
536 }
537 Ok(())
538 }
539}
540
541impl Commands {
542 pub fn is_embedding_heavy(&self) -> bool {
544 matches!(
545 self,
546 Self::Init(_)
547 | Self::Remember(_)
548 | Self::RememberBatch(_)
549 | Self::Recall(_)
550 | Self::HybridSearch(_)
551 | Self::DeepResearch(_)
552 )
553 }
554
555 pub fn uses_cli_slot(&self) -> bool {
556 true
557 }
558
559 pub fn tolerates_missing_embedding_key(&self) -> bool {
566 match self {
567 Self::Init(_) => true,
568 Self::Enrich(args) => {
569 args.status
570 || args.list_dead
571 || args.requeue_dead
572 || args.prune_dead_orphans
573 || args.prune_dead_entity_orphans
574 }
575 _ => false,
576 }
577 }
578}
579
580#[derive(Debug, clap::Args)]
585pub struct CodexModelsArgs {
586 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
588 pub json: bool,
589}
590
591#[derive(Subcommand)]
592pub enum Commands {
593 #[command(after_long_help = "EXAMPLES:\n \
595 # Initialize in current directory (default behavior)\n \
596 sqlite-graphrag init\n\n \
597 # Initialize at a specific path\n \
598 sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n \
599 # Initialize using SQLITE_GRAPHRAG_HOME env var\n \
600 SQLITE_GRAPHRAG_HOME=/data sqlite-graphrag init\n\n\
601 NOTES:\n \
602 - `init` is OPTIONAL: any subsequent CRUD command auto-initializes graphrag.sqlite if missing.\n \
603 - As a side effect, `init` warms a smoke-test embedding via the LLM-only one-shot pipeline.")]
604 Init(init::InitArgs),
605 #[command(after_long_help = "EXAMPLES:\n \
607 # Inline body\n \
608 sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n \
609 # Body from file\n \
610 sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n \
611 # Body from stdin (pipe)\n \
612 cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n \
613 # Enable automatic URL extraction (URL-regex only since v1.0.79)\n \
614 sqlite-graphrag remember --name rich --type note --description \"...\" --body \"...\" --enable-ner")]
615 Remember(remember::RememberArgs),
616 #[command(after_long_help = "EXAMPLES:\n \
618 # Batch create from NDJSON\n \
619 cat memories.ndjson | sqlite-graphrag remember-batch --force-merge --json\n\n \
620 # Atomic batch\n \
621 cat memories.ndjson | sqlite-graphrag remember-batch --transaction --json")]
622 RememberBatch(remember_batch::RememberBatchArgs),
623 Ingest(ingest::IngestArgs),
625 #[command(after_long_help = "EXAMPLES:\n \
627 # Top 10 semantic matches (default)\n \
628 sqlite-graphrag recall \"agent memory\"\n\n \
629 # Top 3 only\n \
630 sqlite-graphrag recall \"agent memory\" -k 3\n\n \
631 # Search across all namespaces\n \
632 sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n \
633 # Disable graph traversal (vector-only)\n \
634 sqlite-graphrag recall \"agent memory\" --no-graph")]
635 Recall(recall::RecallArgs),
636 Read(read::ReadArgs),
638 List(list::ListArgs),
640 Forget(forget::ForgetArgs),
642 Purge(purge::PurgeArgs),
644 Rename(rename::RenameArgs),
646 SplitBody(split_body::SplitBodyArgs),
648 Edit(edit::EditArgs),
650 History(history::HistoryArgs),
652 Restore(restore::RestoreArgs),
654 #[command(after_long_help = "EXAMPLES:\n \
656 # Hybrid search combining KNN + FTS5 BM25 with RRF\n \
657 sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n \
658 # Custom weights for vector vs full-text components\n \
659 sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
660 HybridSearch(hybrid_search::HybridSearchArgs),
661 Health(health::HealthArgs),
663 Migrate(migrate::MigrateArgs),
665 NamespaceDetect(namespace_detect::NamespaceDetectArgs),
667 Optimize(optimize::OptimizeArgs),
669 Stats(stats::StatsArgs),
671 SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
673 Backup(backup::BackupArgs),
675 Vacuum(vacuum::VacuumArgs),
677 Link(link::LinkArgs),
679 Unlink(unlink::UnlinkArgs),
681 #[command(name = "deep-research")]
683 DeepResearch(deep_research::DeepResearchArgs),
684 Related(related::RelatedArgs),
686 Graph(graph_export::GraphArgs),
688 Export(export::ExportArgs),
690 Fts(fts::FtsArgs),
692 Vec(vec::VecArgs),
694 #[command(name = "codex-models")]
700 CodexModels(CodexModelsArgs),
701 PruneRelations(prune_relations::PruneRelationsArgs),
703 #[command(name = "prune-ner")]
705 PruneNer(prune_ner::PruneNerArgs),
706 Slots(slots::SlotsArgs),
708 Pending(pending::PendingArgs),
710 Embedding(embedding::EmbeddingArgs),
712 #[command(name = "pending-embeddings")]
714 PendingEmbeddings(pending_embeddings::PendingEmbeddingsArgs),
715 CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
717 MemoryEntities(memory_entities::MemoryEntitiesArgs),
719 Cache(cache::CacheArgs),
721 #[command(name = "delete-entity")]
723 DeleteEntity(delete_entity::DeleteEntityArgs),
724 Reclassify(reclassify::ReclassifyArgs),
726 #[command(name = "rename-entity")]
728 RenameEntity(rename_entity::RenameEntityArgs),
729 #[command(name = "merge-entities")]
731 MergeEntities(merge_entities::MergeEntitiesArgs),
732 Enrich(enrich::EnrichArgs),
734 #[command(name = "reclassify-relation")]
736 ReclassifyRelation(reclassify_relation::ReclassifyRelationArgs),
737 #[command(name = "normalize-entities")]
739 NormalizeEntities(normalize_entities::NormalizeEntitiesArgs),
740 Completions(completions::CompletionsArgs),
742 #[command(name = "debug-schema", hide = true)]
743 DebugSchema(debug_schema::DebugSchemaArgs),
744 Config(config_cmd::ConfigArgs),
746}
747impl std::fmt::Debug for Commands {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 let name = match self {
755 Self::Init(_) => "Init",
756 Self::Health(_) => "Health",
757 Self::Stats(_) => "Stats",
758 Self::List(_) => "List",
759 Self::Read(_) => "Read",
760 Self::Edit(_) => "Edit",
761 Self::Rename(_) => "Rename",
762 Self::SplitBody(_) => "SplitBody",
763 Self::Restore(_) => "Restore",
764 Self::History(_) => "History",
765 Self::Forget(_) => "Forget",
766 Self::Purge(_) => "Purge",
767 Self::Remember(_) => "Remember",
768 Self::RememberBatch(_) => "RememberBatch",
769 Self::Recall(_) => "Recall",
770 Self::HybridSearch(_) => "HybridSearch",
771 Self::Enrich(_) => "Enrich",
772 Self::Ingest(_) => "Ingest",
773 Self::Optimize(_) => "Optimize",
774 Self::Migrate(_) => "Migrate",
775 Self::SyncSafeCopy(_) => "SyncSafeCopy",
776 Self::Backup(_) => "Backup",
777 Self::Vacuum(_) => "Vacuum",
778 Self::Link(_) => "Link",
779 Self::Unlink(_) => "Unlink",
780 Self::DeepResearch(_) => "DeepResearch",
781 Self::Related(_) => "Related",
782 Self::Graph(_) => "Graph",
783 Self::Export(_) => "Export",
784 Self::Fts(_) => "Fts",
785 Self::Vec(_) => "Vec",
786 Self::CodexModels(_) => "CodexModels",
787 Self::PruneRelations(_) => "PruneRelations",
788 Self::PruneNer(_) => "PruneNer",
789 Self::Slots(_) => "Slots",
790 Self::Pending(_) => "Pending",
791 Self::Embedding(_) => "Embedding",
792 Self::PendingEmbeddings(_) => "PendingEmbeddings",
793 Self::CleanupOrphans(_) => "CleanupOrphans",
794 Self::MemoryEntities(_) => "MemoryEntities",
795 Self::Cache(_) => "Cache",
796 Self::DeleteEntity(_) => "DeleteEntity",
797 Self::Reclassify(_) => "Reclassify",
798 Self::RenameEntity(_) => "RenameEntity",
799 Self::ReclassifyRelation(_) => "ReclassifyRelation",
800 Self::NormalizeEntities(_) => "NormalizeEntities",
801 Self::MergeEntities(_) => "MergeEntities",
802 Self::NamespaceDetect(_) => "NamespaceDetect",
803 Self::Completions(_) => "Completions",
804 Self::DebugSchema(_) => "DebugSchema",
805 Self::Config(_) => "Config",
806 };
807 f.write_str(name)
808 }
809}
810
811#[derive(Copy, Clone, Debug, Default, clap::ValueEnum)]
812pub enum MemoryType {
813 User,
814 Feedback,
815 Project,
816 Reference,
817 Decision,
818 Incident,
819 Skill,
820 #[default]
821 Document,
822 Note,
823}
824
825#[cfg(test)]
826mod heavy_concurrency_tests {
827 use super::*;
828
829 #[test]
830 fn command_heavy_detects_init_and_embeddings() {
831 let init = Cli::try_parse_from(["sqlite-graphrag", "init"]).expect("parse init");
832 assert!(init
833 .command
834 .as_ref()
835 .is_some_and(|c| c.is_embedding_heavy()));
836
837 let remember = Cli::try_parse_from([
838 "sqlite-graphrag",
839 "remember",
840 "--name",
841 "test-memory",
842 "--type",
843 "project",
844 "--description",
845 "desc",
846 ])
847 .expect("parse remember");
848 assert!(remember
849 .command
850 .as_ref()
851 .is_some_and(|c| c.is_embedding_heavy()));
852
853 let recall =
854 Cli::try_parse_from(["sqlite-graphrag", "recall", "query"]).expect("parse recall");
855 assert!(recall
856 .command
857 .as_ref()
858 .is_some_and(|c| c.is_embedding_heavy()));
859
860 let hybrid = Cli::try_parse_from(["sqlite-graphrag", "hybrid-search", "query"])
861 .expect("parse hybrid");
862 assert!(hybrid
863 .command
864 .as_ref()
865 .is_some_and(|c| c.is_embedding_heavy()));
866 }
867
868 #[test]
869 fn command_light_does_not_mark_stats() {
870 let stats = Cli::try_parse_from(["sqlite-graphrag", "stats"]).expect("parse stats");
871 assert!(!stats
872 .command
873 .as_ref()
874 .is_some_and(|c| c.is_embedding_heavy()));
875 }
876}
877
878impl MemoryType {
879 pub fn as_str(&self) -> &'static str {
880 match self {
881 Self::User => "user",
882 Self::Feedback => "feedback",
883 Self::Project => "project",
884 Self::Reference => "reference",
885 Self::Decision => "decision",
886 Self::Incident => "incident",
887 Self::Skill => "skill",
888 Self::Document => "document",
889 Self::Note => "note",
890 }
891 }
892}
893
894#[cfg(test)]
896mod fase_g_parsing_tests {
897 use super::Cli;
898 use clap::Parser;
899
900 #[test]
902 fn enrich_status_optional_operation_and_mode() {
903 assert!(
904 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--status"]).is_ok(),
905 "--status alone must not require --operation/--mode"
906 );
907 assert!(
908 Cli::try_parse_from(["sqlite-graphrag", "enrich", "--list-dead"]).is_ok(),
909 "--list-dead is read-only and must not require --operation/--mode"
910 );
911 assert!(
913 Cli::try_parse_from(["sqlite-graphrag", "enrich"]).is_err(),
914 "bare enrich (no status/list-dead/requeue-dead) must require --operation/--mode"
915 );
916 assert!(Cli::try_parse_from([
918 "sqlite-graphrag",
919 "enrich",
920 "--operation",
921 "memory-bindings",
922 "--mode",
923 "openrouter",
924 ])
925 .is_ok());
926 }
927
928 #[test]
930 fn config_doctor_accepts_json() {
931 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "doctor", "--json"]).is_ok());
932 assert!(Cli::try_parse_from(["sqlite-graphrag", "config", "list-keys", "--json"]).is_ok());
933 }
934
935 #[test]
938 fn remember_description_allows_leading_hyphen() {
939 assert!(Cli::try_parse_from([
940 "sqlite-graphrag",
941 "remember",
942 "--name",
943 "mem",
944 "--type",
945 "note",
946 "--description",
947 "- bullet description",
948 ])
949 .is_ok());
950 }
951
952 #[test]
954 fn remember_batch_accepts_llm_parallelism() {
955 assert!(Cli::try_parse_from([
956 "sqlite-graphrag",
957 "remember-batch",
958 "--llm-parallelism",
959 "4"
960 ])
961 .is_ok());
962 }
963
964 #[test]
967 fn remember_graph_file_combines_with_body_but_conflicts_with_graph_stdin() {
968 assert!(
969 Cli::try_parse_from([
970 "sqlite-graphrag",
971 "remember",
972 "--name",
973 "mem",
974 "--type",
975 "note",
976 "--body",
977 "inline body",
978 "--graph-file",
979 "/tmp/graph.json",
980 ])
981 .is_ok(),
982 "--body + --graph-file must coexist"
983 );
984 assert!(
985 Cli::try_parse_from([
986 "sqlite-graphrag",
987 "remember",
988 "--name",
989 "mem",
990 "--type",
991 "note",
992 "--graph-file",
993 "/tmp/graph.json",
994 "--graph-stdin",
995 ])
996 .is_err(),
997 "--graph-file conflicts with --graph-stdin"
998 );
999 }
1000}