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, Subcommand, ValueEnum};
7
8use defect_config::{
9 CliOverrides, LogFormat, 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/// Values for `--log-format`. Mirrors [`LogFormat`] locally so that clap can render the
37/// possible values directly; the config crate does not depend on clap.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
39pub enum LogFormatArg {
40 Text,
41 Jsonl,
42}
43
44impl From<LogFormatArg> for LogFormat {
45 fn from(arg: LogFormatArg) -> Self {
46 match arg {
47 LogFormatArg::Text => LogFormat::Text,
48 LogFormatArg::Jsonl => LogFormat::Jsonl,
49 }
50 }
51}
52
53/// Output format for stdout in `--message` single-turn mode.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
55pub enum OutputFormat {
56 /// Plain text, no ANSI: assistant body to stdout, thoughts/tools to stderr.
57 #[default]
58 Text,
59 /// One JSON line (NDJSON) per `AgentEvent` to stdout.
60 Json,
61 /// Silent mode; only prints the final result or error at the end.
62 Quiet,
63}
64
65/// Headless agent over ACP/stdio.
66#[derive(Debug, Parser)]
67#[command(
68 name = "defect",
69 version,
70 about = "Headless agent over ACP/stdio",
71 long_about = "defect — headless agent over ACP/stdio.\n\n\
72 Auth env: ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY.\n\
73 Logging: RUST_LOG controls tracing-subscriber EnvFilter (default: info); \
74 --log-format selects text or jsonl output."
75)]
76pub struct CliArgs {
77 /// LLM provider to use. CLI flag takes precedence over the `DEFECT_PROVIDER`
78 /// environment variable and config file.
79 #[arg(long, env = "DEFECT_PROVIDER")]
80 pub provider: Option<String>,
81
82 /// Override the default model ID. CLI flag takes precedence over the `DEFECT_MODEL`
83 /// environment variable.
84 #[arg(long, env = "DEFECT_MODEL")]
85 pub model: Option<String>,
86
87 /// Override the sandbox permission mode. CLI flag takes precedence over config
88 /// `[sandbox].mode`. Useful for CI: `--sandbox open` runs every tool without
89 /// prompting. Note that `--repl` always forces `open` regardless.
90 #[arg(long, value_enum)]
91 pub sandbox: Option<SandboxModeArg>,
92
93 /// Shortcut for `--sandbox open`: grants maximum permissions and runs every tool
94 /// without prompting. Mutually exclusive with `--sandbox`.
95 #[arg(long, conflicts_with = "sandbox")]
96 pub yolo: bool,
97
98 /// Run the entire session under a named subagent profile, located in
99 /// `.defect/agents/<name>/` or `~/.config/defect/agents/<name>/`.
100 /// The profile's model, system prompt, and tool allowlist become the session root.
101 /// The CLI flag takes precedence over the `DEFECT_PROFILE` environment variable.
102 #[arg(long, env = "DEFECT_PROFILE")]
103 pub profile: Option<String>,
104
105 /// Format for the tracing/log output (stderr). `text` (default) is human-readable;
106 /// `jsonl` emits one JSON object per log line. Takes precedence over config
107 /// `[tracing].format`. This controls diagnostic logs, not the `--format` stdout event
108 /// stream.
109 #[arg(long, value_enum)]
110 pub log_format: Option<LogFormatArg>,
111
112 /// Additional dotted-path config overrides; may be repeated.
113 #[arg(long = "config", value_name = "KEY=VALUE")]
114 pub config_override: Vec<String>,
115
116 /// Resume a previous session. With a `SESSION_ID`, resume that specific session; bare
117 /// `--resume` resumes the most recently active session for the current working
118 /// directory. In ACP mode, the resumed session is returned on the first
119 /// `session/new`; in `--repl` mode, it is loaded directly.
120 #[arg(long, value_name = "SESSION_ID")]
121 pub resume: Option<Option<String>>,
122
123 /// Sandbox mode: ignore global/user config and store all state (config, sessions)
124 /// under `<repo-root>/.defect/`. The user-level `~/.config/defect` config, agents,
125 /// and skills are skipped entirely.
126 #[arg(long)]
127 pub local: bool,
128
129 /// Run a minimal in-process REPL on stdin/stdout instead of the ACP server. Requires
130 /// the `repl` build feature (enabled by default); a binary built with
131 /// `--no-default-features` rejects this flag at runtime.
132 #[arg(long)]
133 pub repl: bool,
134
135 /// Run a single prompt turn headlessly and exit (CI / scripting). The assistant
136 /// output goes to stdout; the process exit code reflects the turn outcome. A value of
137 /// `-`, or no value while stdin is piped, reads the prompt from stdin. Combine with
138 /// `--resume` to continue a previous session. Mutually exclusive with `--repl`.
139 /// Requires the `oneshot` build feature (on by default).
140 #[arg(long, value_name = "PROMPT", conflicts_with = "repl")]
141 pub message: Option<String>,
142
143 /// Output format for `--message` / `--goal` mode.
144 #[arg(long, value_enum, default_value_t = OutputFormat::default())]
145 pub format: OutputFormat,
146
147 /// Run a goal-driven autonomous loop and exit (for CI / scripting). The agent works
148 /// across multiple turns until it calls the `goal_done` tool (goal achieved) or
149 /// reaches `--max-turns`. Like `--message`, but continues until the goal is reached
150 /// instead of stopping after one turn. Reads from stdin if the value is `-` or
151 /// omitted while piped. Mutually exclusive with `--message` and `--repl`. Requires
152 /// the `oneshot` build feature.
153 #[arg(long, value_name = "OBJECTIVE", conflicts_with_all = ["message", "repl"])]
154 pub goal: Option<String>,
155
156 /// Maximum number of times the goal-gate may keep the agent going before giving up
157 /// (maps to `[turn].max_hook_continues`). Only meaningful with `--goal`. When
158 /// exceeded, the run exits with a non-zero (exhausted) code.
159 #[arg(long, value_name = "N")]
160 pub max_turns: Option<u32>,
161
162 /// Optional subcommand. When omitted, `defect` runs as an agent (ACP server by
163 /// default, or one of the `--repl` / `--message` / `--goal` modes). Subcommands are
164 /// for one-off management tasks that exit without starting an agent.
165 #[command(subcommand)]
166 pub command: Option<Command>,
167}
168
169/// Management subcommands that run instead of the agent and then exit.
170#[derive(Debug, Subcommand)]
171pub enum Command {
172 /// Scan the environment for provider API keys and write a global config.toml.
173 Init(InitArgs),
174}
175
176/// Arguments for `defect init`.
177#[derive(Debug, Default, clap::Args)]
178pub struct InitArgs {
179 /// Non-interactive: skip all prompts and write the config from detected env keys.
180 /// When multiple provider keys are present, `--default-provider` must also be given
181 /// (defect never guesses which provider should be the default).
182 #[arg(long)]
183 pub yes: bool,
184
185 /// Overwrite an existing global config.toml. Without this, init refuses to clobber
186 /// an existing file.
187 #[arg(long)]
188 pub force: bool,
189
190 /// The provider to record as `[default] provider`. Required by `--yes` when more
191 /// than one provider key is detected; otherwise optional (defaults to the sole
192 /// detected provider, or is chosen interactively).
193 #[arg(long, value_name = "PROVIDER")]
194 pub default_provider: Option<String>,
195
196 /// The model to record as `[default] model` for the default provider. Must be one of
197 /// the models the provider's API actually returns (init validates against the live
198 /// list). When omitted: chosen interactively, or under `--yes` the first model the
199 /// provider lists.
200 #[arg(long, value_name = "MODEL")]
201 pub default_model: Option<String>,
202}
203
204impl CliArgs {
205 /// Translates CLI flags into [`CliOverrides`] and feeds them to
206 /// `defect_config::load_config`.
207 pub fn to_overrides(&self) -> anyhow::Result<CliOverrides> {
208 let config_overrides = self
209 .config_override
210 .iter()
211 .map(|spec| parse_cli_override(spec).map_err(|e| anyhow::anyhow!("{e}")))
212 .collect::<anyhow::Result<Vec<_>>>()?;
213 // `--yolo` is syntactic sugar for `--sandbox open` (clap ensures they are
214 // mutually exclusive).
215 let sandbox = if self.yolo {
216 Some(SandboxMode::Open)
217 } else {
218 self.sandbox.map(SandboxMode::from)
219 };
220 Ok(CliOverrides {
221 provider: self.provider.as_deref().map(ConfigProviderKind::from),
222 model: self.model.clone(),
223 sandbox,
224 log_format: self.log_format.map(LogFormat::from),
225 config_overrides,
226 })
227 }
228}