use clap::{Parser, ValueEnum};
use compact_str::CompactString;
use crate::config;
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum, Default)]
#[clap(rename_all = "kebab-case")]
pub enum OutputFormat {
#[default]
Text,
Json,
StreamJson,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum AutoConfirmMode {
Yes,
No,
}
#[derive(Parser, Debug)]
#[command(name = "dirge", version, about = "Minimal coding agent")]
pub struct Cli {
#[arg(short = 'p', long = "print", help = "Print response and exit")]
pub print: bool,
#[arg(
long = "output-format",
value_enum,
default_value_t = OutputFormat::Text,
requires = "print",
help = "Output format for --print mode (text | json | stream-json)"
)]
pub output_format: OutputFormat,
#[arg(short = 'c', long = "continue", help = "Continue most recent session")]
pub continue_session: bool,
#[arg(short = 'r', long = "resume", help = "Browse and select a session")]
pub resume: bool,
#[arg(
long = "session",
help = "Resume a session by id/prefix, or create one with this exact id if none exists (stable id for scripts / the shell plugin)"
)]
pub session: Option<String>,
#[arg(long = "no-session", help = "Ephemeral mode, do not save")]
pub no_session: bool,
#[arg(long = "provider", env = "DIRGE_PROVIDER", help = "API provider")]
pub provider: Option<String>,
#[arg(long = "model", env = "DIRGE_MODEL", help = "Model name")]
pub model: Option<String>,
#[arg(
long = "api-key",
help = "API key for the provider (WARNING: visible to other users via ps/htop; prefer env vars or --api-key-file)"
)]
pub api_key: Option<String>,
#[arg(
long = "api-key-file",
value_name = "PATH",
help = "Read API key from a file (preferred over --api-key; file contents must be the raw key, with trailing whitespace stripped)"
)]
pub api_key_file: Option<std::path::PathBuf>,
#[arg(
long = "api-key-stdin",
help = "Read API key from stdin at startup (single line; mutually exclusive with --api-key-file)"
)]
pub api_key_stdin: bool,
#[arg(long = "max-tokens", help = "Maximum tokens in response")]
pub max_tokens: Option<u64>,
#[arg(long = "max-agent-turns", help = "Maximum agent turns")]
pub max_agent_turns: Option<usize>,
#[arg(long = "temperature", help = "Model temperature (0.0 to 2.0)")]
pub temperature: Option<f64>,
#[arg(long = "no-tools", help = "Disable all tools")]
pub no_tools: bool,
#[cfg(feature = "lsp")]
#[arg(
long = "no-lsp",
help = "Disable LSP integration (no diagnostics on edit/write, no `lsp` agent tool)"
)]
pub no_lsp: bool,
#[arg(long = "no-color", help = "Disable colored TUI output")]
pub no_color: bool,
#[arg(
short = 'v',
long = "verbose",
help = "Enable verbose logging (debug for dirge, warn for plugin hooks; equivalent to RUST_LOG=dirge=debug,dirge::plugin=warn). RUST_LOG env takes precedence if set."
)]
pub verbose: bool,
#[arg(
long = "restrictive",
short = 'R',
help = "Default all tools to ask for approval"
)]
pub restrictive: bool,
#[arg(
long = "accept-all",
help = "Auto-accept all operations within the working directory"
)]
pub accept_all: bool,
#[arg(
long = "yolo",
help = "Auto-accept ALL operations without any restriction"
)]
pub yolo: bool,
#[arg(
long = "sandbox",
num_args = 0..=1,
default_missing_value = "none",
require_equals = false,
help = "Run bash in an isolated sandbox: 'bwrap' (bubblewrap), 'microvm' (hardware VM via libkrun), or 'none' (default, no sandbox)"
)]
pub sandbox: Option<String>,
#[arg(
long = "microvm-image",
value_name = "IMAGE",
help = "OCI image or local reference for the microVM sandbox (e.g. 'docker.io/library/alpine:3.21', 'local://my-image:tag')"
)]
pub microvm_image: Option<String>,
#[arg(
long = "no-context-files",
short = 'n',
help = "Disable AGENTS.md loading"
)]
pub no_context_files: bool,
#[cfg(feature = "loop")]
#[arg(
long = "loop",
help = "Run in headless loop mode (requires --loop-prompt or message)"
)]
pub loop_mode: bool,
#[cfg(feature = "acp")]
#[arg(
long = "acp",
help = "Enable ACP (Agent Communication Protocol) support"
)]
pub acp_enabled: bool,
#[cfg(feature = "loop")]
#[arg(long = "loop-prompt", help = "Prompt for each loop iteration")]
pub loop_prompt: Option<String>,
#[cfg(feature = "loop")]
#[arg(long = "loop-plan", help = "Plan file path [default: LOOP_PLAN.md]")]
pub loop_plan: Option<std::path::PathBuf>,
#[cfg(feature = "loop")]
#[arg(long = "loop-max", help = "Maximum number of iterations")]
pub loop_max: Option<u32>,
#[cfg(feature = "loop")]
#[arg(
long = "loop-run",
help = "Validation command to run after each iteration"
)]
pub loop_run: Option<String>,
#[arg(
long = "auto-confirm",
value_enum,
help = "Auto-respond to plugin harness/confirm and harness/select dialogs in headless modes. Without this flag, dialogs hang waiting for an interactive UI."
)]
pub auto_confirm: Option<AutoConfirmMode>,
#[arg(
long = "prompt",
value_name = "NAME",
help = "Lock the session to a specific prompt at launch (e.g. --prompt plan)"
)]
pub prompt: Option<String>,
#[arg(help = "Prompt message(s)")]
pub message: Vec<String>,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(clap::Subcommand, Debug)]
pub enum Command {
Sandbox {
#[command(subcommand)]
action: SandboxAction,
},
}
#[derive(clap::Subcommand, Debug)]
pub enum SandboxAction {
Check,
Setup {
#[arg(long = "image")]
image: Option<String>,
},
}
impl Cli {
pub fn resolve_model(&self, cfg: &config::Config) -> CompactString {
if let Some(m) = self.model.as_deref() {
return CompactString::new(m);
}
if let Some((_, entry)) = cfg.resolve_role(config::ConfigRole::Default)
&& let Some(m) = entry.model
{
return CompactString::new(m);
}
CompactString::new("deepseek/deepseek-v4-flash")
}
pub fn resolve_provider(&self, cfg: &config::Config) -> CompactString {
if let Some(p) = self.provider.as_deref().or(cfg.provider.as_deref()) {
return CompactString::new(p);
}
if let Some(detected) = crate::provider::auto_detect_provider() {
eprintln!(
"info: provider auto-detected from environment: {} (set `--provider` or `provider` in config.json to override)",
detected,
);
return CompactString::new(detected);
}
CompactString::new("openrouter")
}
pub fn resolve_max_tokens(&self, cfg: &config::Config) -> u64 {
self.max_tokens.or(cfg.max_tokens).unwrap_or(8192)
}
pub fn resolve_temperature(&self, cfg: &config::Config) -> Option<f64> {
if let Some(t) = self.temperature {
return Some(t);
}
if let Some((_, entry)) = cfg.resolve_role(config::ConfigRole::Default)
&& let Some(t) = entry.options_temperature()
{
return Some(t);
}
cfg.temperature
}
pub fn resolve_max_agent_turns(&self, cfg: &config::Config) -> usize {
self.max_agent_turns.or(cfg.max_agent_turns).unwrap_or(100)
}
pub fn resolve_no_context_files(&self, cfg: &config::Config) -> bool {
self.no_context_files || cfg.no_context_files.unwrap_or(false)
}
pub fn resolve_no_tools(&self, cfg: &config::Config) -> bool {
self.no_tools || cfg.no_tools.unwrap_or(false)
}
#[cfg(feature = "lsp")]
pub fn resolve_lsp_enabled(&self, cfg: &config::Config) -> bool {
if self.no_lsp || self.no_tools {
return false;
}
match &cfg.lsp {
Some(c) => c.is_enabled(),
None => true, }
}
pub fn resolve_sandbox(&self, cfg: &config::Config) -> crate::sandbox::SandboxMode {
if let Some(val) = self.sandbox.as_deref() {
return crate::sandbox::SandboxMode::parse(Some(val));
}
cfg.resolve_sandbox_mode()
}
pub fn resolve_microvm_image(&self, cfg: &config::Config) -> Option<String> {
self.microvm_image
.clone()
.or_else(|| cfg.resolve_microvm_image())
}
}