use crate::cli::MemoryType;
use crate::output::JsonOutputFormat;
use std::path::PathBuf;
#[derive(clap::Args)]
#[command(after_long_help = "EXAMPLES:\n \
# Ingest every Markdown file under ./docs as `document` memories\n \
sqlite-graphrag ingest ./docs --type document\n\n \
# Ingest .txt files recursively under ./notes\n \
sqlite-graphrag ingest ./notes --type note --pattern '*.txt' --recursive\n\n \
# Namespace derived names with a kebab-case prefix (projx-<derived>)\n \
sqlite-graphrag ingest ./docs --name-prefix projx- --dry-run\n\n \
# Enable automatic URL extraction (URL-regex only since v1.0.79)\n \
sqlite-graphrag ingest ./big-corpus --type reference --enable-ner\n\n \
# Preview file-to-name mapping without ingesting\n \
sqlite-graphrag ingest ./docs --dry-run\n\n \
# LLM-curated extraction via Claude Code CLI\n \
sqlite-graphrag ingest ./docs --mode claude-code --recursive --json\n\n \
# Resume interrupted claude-code ingest\n \
sqlite-graphrag ingest ./docs --mode claude-code --resume --json\n\n \
# Claude Code with budget cap and custom timeout\n \
sqlite-graphrag ingest ./docs --mode claude-code --max-cost-usd 5.00 --claude-timeout 600 --json\n\n \
AUTHENTICATION:\n \
--mode claude-code: Uses existing Claude Code authentication.\n \
OAuth (Pro/Max/Team): works automatically from ~/.claude/.credentials.json\n \
API key: set ANTHROPIC_API_KEY for faster startup (optional)\n\n \
--mode codex: Uses existing Codex CLI authentication.\n \
Device auth: run `codex auth login` first\n \
API key: set OPENAI_API_KEY (optional)\n\n \
NOTES:\n \
Each file becomes a separate memory. Names derive from file basenames\n \
(kebab-case, lowercase, ASCII). Output is NDJSON: one JSON object per file,\n \
followed by a final summary line with counts. Per-file errors are reported\n \
inline and processing continues unless --fail-fast is set.")]
pub struct IngestArgs {
#[arg(
value_name = "DIR",
help = "Directory to ingest recursively (each matching file becomes a memory)"
)]
pub dir: PathBuf,
#[arg(long, value_enum, default_value_t = MemoryType::Document)]
pub r#type: MemoryType,
#[arg(long, default_value = "*.md")]
pub pattern: String,
#[arg(long, default_value_t = false)]
pub recursive: bool,
#[arg(
long,
value_parser = crate::parsers::parse_bool_flexible,
action = clap::ArgAction::Set,
num_args = 0..=1,
default_missing_value = "true",
default_value = "false",
help = "Enable automatic URL-regex extraction (URL-regex only since v1.0.79)"
)]
pub enable_ner: bool,
#[arg(
long,
default_value_t = true,
overrides_with = "no_auto_describe",
help = "Derive memory description from the first meaningful body line instead of the legacy `ingested from <path>` placeholder."
)]
pub auto_describe: bool,
#[arg(
long = "no-auto-describe",
default_value_t = false,
help = "Disable `--auto-describe` and fall back to the legacy `ingested from <path>` description placeholder."
)]
pub no_auto_describe: bool,
#[arg(long, default_value_t = false, hide = true)]
pub skip_extraction: bool,
#[arg(long, default_value_t = false)]
pub fail_fast: bool,
#[arg(long, default_value_t = false)]
pub dry_run: bool,
#[arg(long, default_value_t = 10_000)]
pub max_files: usize,
#[arg(long)]
pub namespace: Option<String>,
#[arg(long)]
pub db: Option<String>,
#[arg(long, value_enum, default_value_t = JsonOutputFormat::Json)]
pub format: JsonOutputFormat,
#[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
pub json: bool,
#[arg(
long,
help = "Number of files to extract+embed in parallel; default = max(1, cpus/2).min(4)"
)]
pub ingest_parallelism: Option<usize>,
#[arg(
long,
default_value_t = false,
help = "Forces single-threaded ingest (--ingest-parallelism 1) to reduce RSS pressure. \
Recommended for environments with <4 GB available RAM or container/cgroup \
constraints. Trade-off: 3-4x longer wall time. Also honored via \
XDG ingest.low_memory=1."
)]
pub low_memory: bool,
#[arg(long, default_value_t = crate::constants::DEFAULT_MAX_RSS_MB,
help = "Maximum process RSS in MiB; abort if exceeded during embedding (default: 8192)")]
pub max_rss_mb: u64,
#[arg(long, default_value_t = 2, value_name = "N",
value_parser = clap::value_parser!(u64).range(1..=32),
help = "Maximum simultaneous LLM embedding subprocesses per file (default: 2, clamp [1,32])")]
pub llm_parallelism: u64,
#[arg(long, default_value_t = crate::constants::DERIVED_NAME_MAX_LEN,
help = "Maximum length for derived memory names (default: 60)")]
pub max_name_length: usize,
#[arg(
long,
value_name = "PREFIX",
help = "Kebab-case prefix applied to every derived memory name (e.g. 'projx-')"
)]
pub name_prefix: Option<String>,
#[arg(long, value_enum, default_value_t = IngestMode::None)]
pub mode: IngestMode,
#[arg(long)]
pub claude_binary: Option<std::path::PathBuf>,
#[arg(long)]
pub claude_model: Option<String>,
#[arg(long, default_value_t = false)]
pub resume: bool,
#[arg(long, default_value_t = false)]
pub retry_failed: bool,
#[arg(long, default_value_t = false)]
pub keep_queue: bool,
#[arg(long)]
pub queue_db: Option<String>,
#[arg(long, default_value_t = 60)]
pub rate_limit_wait: u64,
#[arg(long)]
pub max_cost_usd: Option<f64>,
#[arg(
long,
default_value_t = 300,
help = "Timeout in seconds for each claude -p invocation (default: 300)"
)]
pub claude_timeout: u64,
#[arg(
long,
help = "Explicit path to the Codex CLI binary (only with --mode codex)"
)]
pub codex_binary: Option<PathBuf>,
#[arg(
long,
help = "Model override for Codex extraction (e.g. o4-mini, gpt-5.1-codex)"
)]
pub codex_model: Option<String>,
#[arg(
long,
default_value_t = 300,
help = "Timeout in seconds for each codex exec invocation (default: 300)"
)]
pub codex_timeout: u64,
#[arg(long, value_name = "PATH")]
pub opencode_binary: Option<PathBuf>,
#[arg(
long,
value_name = "MODEL",
help = "Model override for OpenCode extraction"
)]
pub opencode_model: Option<String>,
#[arg(
long,
value_name = "SECONDS",
default_value_t = 300,
help = "Timeout in seconds for each opencode run invocation (default: 300)"
)]
pub opencode_timeout: u64,
#[arg(long, value_name = "SECONDS")]
pub wait_job_singleton: Option<u64>,
#[arg(long, default_value_t = false)]
pub force_job_singleton: bool,
#[arg(
long,
default_value_t = false,
help = "Run enrich --operation memory-bindings after all files are ingested"
)]
pub enrich_after: bool,
#[arg(
long,
default_value_t = false,
help = "Update existing memories on name collision instead of skipping (idempotent re-ingest)"
)]
pub force_merge: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum IngestMode {
None,
ClaudeCode,
Codex,
#[value(name = "opencode")]
Opencode,
}
pub(crate) fn low_memory_setting_enabled() -> bool {
match crate::config::get_setting("ingest.low_memory") {
Ok(Some(v)) if v.is_empty() => false,
Ok(Some(v)) => match v.to_lowercase().as_str() {
"1" | "true" | "yes" | "on" => true,
"0" | "false" | "no" | "off" => false,
other => {
tracing::warn!(
target: "ingest",
value = %other,
"ingest.low_memory value not recognized; treating as disabled"
);
false
}
},
_ => false,
}
}
pub(crate) fn resolve_parallelism(low_memory_flag: bool, ingest_parallelism: Option<usize>) -> usize {
let setting_flag = low_memory_setting_enabled();
let low_memory = low_memory_flag || setting_flag;
if low_memory {
if let Some(n) = ingest_parallelism {
if n > 1 {
tracing::warn!(
target: "ingest",
requested = n,
"--ingest-parallelism overridden by --low-memory; using 1"
);
}
}
if low_memory_flag {
tracing::info!(
target: "ingest",
source = "flag",
"low-memory mode enabled: forcing --ingest-parallelism 1"
);
} else {
tracing::info!(
target: "ingest",
source = "xdg",
"low-memory mode enabled via XDG ingest.low_memory: forcing --ingest-parallelism 1"
);
}
return 1;
}
ingest_parallelism
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|v| v.get() / 2)
.unwrap_or(1)
.clamp(1, 4)
})
.max(1)
}