defect_cli/args.rs
1//! CLI argument parsing.
2//!
3//! Aligned with `defect-config`'s `LoadConfigOptions::cli` — CLI flags take precedence.
4//! CLI arguments — see config and `CliOverrides`.
5
6use clap::{Parser, ValueEnum};
7
8use defect_config::{
9 CliOverrides, ProviderKind as ConfigProviderKind, SandboxMode, parse_cli_override,
10};
11
12/// Values for `--sandbox`. Mirrors [`SandboxMode`] locally so that clap can render the
13/// possible values directly;
14/// the config crate does not depend on clap, so it does not derive `ValueEnum` there
15/// (following the same
16/// "CLI-side parsing" pattern used by providers).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
18pub enum SandboxModeArg {
19 ReadOnly,
20 AskWrites,
21 Open,
22 DenyAll,
23}
24
25impl From<SandboxModeArg> for SandboxMode {
26 fn from(arg: SandboxModeArg) -> Self {
27 match arg {
28 SandboxModeArg::ReadOnly => SandboxMode::ReadOnly,
29 SandboxModeArg::AskWrites => SandboxMode::AskWrites,
30 SandboxModeArg::Open => SandboxMode::Open,
31 SandboxModeArg::DenyAll => SandboxMode::DenyAll,
32 }
33 }
34}
35
36/// Output format for stdout in `--message` single-turn mode.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
38pub enum OutputFormat {
39 /// Plain text, no ANSI: assistant body to stdout, thoughts/tools to stderr.
40 #[default]
41 Text,
42 /// One JSON line (NDJSON) per `AgentEvent` to stdout.
43 Json,
44 /// Silent mode; only prints the final result or error at the end.
45 Quiet,
46}
47
48/// Headless agent over ACP/stdio.
49#[derive(Debug, Parser)]
50#[command(
51 name = "defect",
52 version,
53 about = "Headless agent over ACP/stdio",
54 long_about = "defect — headless agent over ACP/stdio.\n\n\
55 Auth env: ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY.\n\
56 Logging: RUST_LOG controls tracing-subscriber EnvFilter (default: info)."
57)]
58pub struct CliArgs {
59 /// LLM provider to use. CLI flag takes precedence over the `DEFECT_PROVIDER`
60 /// environment variable and config file.
61 #[arg(long, env = "DEFECT_PROVIDER")]
62 pub provider: Option<String>,
63
64 /// Override the default model ID. CLI flag takes precedence over the `DEFECT_MODEL`
65 /// environment variable.
66 #[arg(long, env = "DEFECT_MODEL")]
67 pub model: Option<String>,
68
69 /// Override the sandbox permission mode. CLI flag takes precedence over config
70 /// `[sandbox].mode`. Useful for CI: `--sandbox open` runs every tool without
71 /// prompting. Note that `--repl` always forces `open` regardless.
72 #[arg(long, value_enum)]
73 pub sandbox: Option<SandboxModeArg>,
74
75 /// Shortcut for `--sandbox open`: grants maximum permissions and runs every tool
76 /// without prompting. Mutually exclusive with `--sandbox`.
77 #[arg(long, conflicts_with = "sandbox")]
78 pub yolo: bool,
79
80 /// Run the entire session under a named subagent profile, located in
81 /// `.defect/agents/<name>/` or `~/.config/defect/agents/<name>/`.
82 /// The profile's model, system prompt, and tool allowlist become the session root.
83 /// The CLI flag takes precedence over the `DEFECT_PROFILE` environment variable.
84 #[arg(long, env = "DEFECT_PROFILE")]
85 pub profile: Option<String>,
86
87 /// Additional dotted-path config overrides; may be repeated.
88 #[arg(long = "config", value_name = "KEY=VALUE")]
89 pub config_override: Vec<String>,
90
91 /// Resume a previous session. With a `SESSION_ID`, resume that specific session; bare
92 /// `--resume` resumes the most recently active session for the current working
93 /// directory. In ACP mode, the resumed session is returned on the first
94 /// `session/new`; in `--repl` mode, it is loaded directly.
95 #[arg(long, value_name = "SESSION_ID")]
96 pub resume: Option<Option<String>>,
97
98 /// Sandbox mode: ignore global/user config and store all state (config, sessions)
99 /// under `<repo-root>/.defect/`. The user-level `~/.config/defect` config, agents,
100 /// and skills are skipped entirely.
101 #[arg(long)]
102 pub local: bool,
103
104 /// Run a minimal in-process REPL on stdin/stdout instead of the ACP server. Requires
105 /// the `repl` build feature (enabled by default); a binary built with
106 /// `--no-default-features` rejects this flag at runtime.
107 #[arg(long)]
108 pub repl: bool,
109
110 /// Run a single prompt turn headlessly and exit (CI / scripting). The assistant
111 /// output goes to stdout; the process exit code reflects the turn outcome. A value of
112 /// `-`, or no value while stdin is piped, reads the prompt from stdin. Combine with
113 /// `--resume` to continue a previous session. Mutually exclusive with `--repl`.
114 /// Requires the `oneshot` build feature (on by default).
115 #[arg(long, value_name = "PROMPT", conflicts_with = "repl")]
116 pub message: Option<String>,
117
118 /// Output format for `--message` / `--goal` mode.
119 #[arg(long, value_enum, default_value_t = OutputFormat::default())]
120 pub format: OutputFormat,
121
122 /// Run a goal-driven autonomous loop and exit (for CI / scripting). The agent works
123 /// across multiple turns until it calls the `goal_done` tool (goal achieved) or
124 /// reaches `--max-turns`. Like `--message`, but continues until the goal is reached
125 /// instead of stopping after one turn. Reads from stdin if the value is `-` or
126 /// omitted while piped. Mutually exclusive with `--message` and `--repl`. Requires
127 /// the `oneshot` build feature.
128 #[arg(long, value_name = "OBJECTIVE", conflicts_with_all = ["message", "repl"])]
129 pub goal: Option<String>,
130
131 /// Maximum number of times the goal-gate may keep the agent going before giving up
132 /// (maps to `[turn].max_hook_continues`). Only meaningful with `--goal`. When
133 /// exceeded, the run exits with a non-zero (exhausted) code.
134 #[arg(long, value_name = "N")]
135 pub max_turns: Option<u32>,
136}
137
138impl CliArgs {
139 /// Translates CLI flags into [`CliOverrides`] and feeds them to
140 /// `defect_config::load_config`.
141 pub fn to_overrides(&self) -> anyhow::Result<CliOverrides> {
142 let config_overrides = self
143 .config_override
144 .iter()
145 .map(|spec| parse_cli_override(spec).map_err(|e| anyhow::anyhow!("{e}")))
146 .collect::<anyhow::Result<Vec<_>>>()?;
147 // `--yolo` is syntactic sugar for `--sandbox open` (clap ensures they are
148 // mutually exclusive).
149 let sandbox = if self.yolo {
150 Some(SandboxMode::Open)
151 } else {
152 self.sandbox.map(SandboxMode::from)
153 };
154 Ok(CliOverrides {
155 provider: self.provider.as_deref().map(ConfigProviderKind::from),
156 model: self.model.clone(),
157 sandbox,
158 config_overrides,
159 })
160 }
161}