Skip to main content

zagens_runtime/cli/
args.rs

1//! CLI argument types (`clap`) — B3 split from `main.rs`.
2
3use std::path::PathBuf;
4
5use anyhow::Result;
6use clap::{Args, Parser, Subcommand};
7use clap_complete::Shell;
8
9use crate::config::Config;
10
11#[derive(Parser, Debug)]
12#[command(
13    name = "zagens",
14    author,
15    version,
16    about = "Zagens headless CLI for DeepSeek agent runtime",
17    long_about = "Scriptable CLI for the Zagens agent runtime.\n\nRun `zagens exec '…'` for one-shot tasks, `zagens doctor` for diagnostics, or `zagens serve --http` for the local API.\n\nNot affiliated with DeepSeek Inc."
18)]
19pub struct Cli {
20    /// Subcommand to run
21    #[command(subcommand)]
22    pub command: Option<Commands>,
23
24    #[command(flatten)]
25    pub feature_toggles: FeatureToggles,
26
27    /// Send a one-shot prompt (non-interactive)
28    #[arg(short, long)]
29    pub prompt: Option<String>,
30
31    /// YOLO mode: enable agent tools + shell execution
32    #[arg(long)]
33    pub yolo: bool,
34
35    /// Maximum number of concurrent sub-agents (1-20)
36    #[arg(long)]
37    pub max_subagents: Option<usize>,
38
39    /// Path to config file
40    #[arg(long, global = true)]
41    pub config: Option<PathBuf>,
42
43    /// Enable verbose logging
44    #[arg(short, long, global = true)]
45    pub verbose: bool,
46
47    /// Config profile name
48    #[arg(long, global = true)]
49    pub profile: Option<String>,
50
51    /// Workspace directory for file operations
52    #[arg(short, long, global = true)]
53    pub workspace: Option<PathBuf>,
54
55    /// Resume a previous session by ID or prefix
56    #[arg(short, long)]
57    pub resume: Option<String>,
58
59    /// Continue the most recent session in this workspace
60    #[arg(short = 'c', long = "continue")]
61    pub continue_session: bool,
62
63    /// Disable the alternate screen buffer (inline mode)
64    #[arg(long = "no-alt-screen")]
65    pub no_alt_screen: bool,
66
67    /// Enable TUI mouse capture for internal scrolling and transcript selection
68    /// (default off on Windows)
69    #[arg(long = "mouse-capture", conflicts_with = "no_mouse_capture")]
70    pub mouse_capture: bool,
71
72    /// Disable TUI mouse capture so terminal-native text selection works
73    #[arg(long = "no-mouse-capture", conflicts_with = "mouse_capture")]
74    pub no_mouse_capture: bool,
75
76    /// Skip onboarding screens
77    #[arg(long)]
78    pub skip_onboarding: bool,
79
80    /// Start a fresh session, ignoring any crash-recovery checkpoint
81    #[arg(long = "fresh")]
82    pub fresh: bool,
83
84    /// Skip loading project-level config from $WORKSPACE/.zagens/config.toml
85    #[arg(long = "no-project-config", global = true)]
86    pub no_project_config: bool,
87}
88
89#[derive(Subcommand, Debug, Clone)]
90#[allow(clippy::large_enum_variant)]
91pub enum Commands {
92    /// Run system diagnostics and check configuration
93    Doctor(DoctorArgs),
94    /// Bootstrap MCP config and/or skills directories
95    Setup(SetupArgs),
96    /// Generate shell completions
97    Completions {
98        /// Shell to generate completions for
99        #[arg(value_enum)]
100        shell: Shell,
101    },
102    /// List saved sessions
103    Sessions {
104        /// Maximum number of sessions to display
105        #[arg(short, long, default_value = "20")]
106        limit: usize,
107        /// Search sessions by title
108        #[arg(short, long)]
109        search: Option<String>,
110    },
111    /// Create default AGENTS.md in current directory
112    Init,
113    /// Save a DeepSeek API key to the shared user config
114    Login {
115        /// API key to store (otherwise read from stdin)
116        #[arg(long)]
117        api_key: Option<String>,
118    },
119    /// Remove the saved API key
120    Logout,
121    /// List available models from the configured API endpoint
122    Models(ModelsArgs),
123    /// Run a non-interactive prompt
124    Exec(ExecArgs),
125    /// Run a code review over a git diff
126    Review(ReviewArgs),
127    /// Open the TUI pre-seeded with a GitHub PR's title, body, and diff (#451)
128    Pr {
129        /// PR number
130        #[arg(value_name = "NUMBER")]
131        number: u32,
132        /// Repository in `owner/name` form. Defaults to the current
133        /// workspace's `gh` config (i.e. the repo gh thinks you're in).
134        #[arg(short = 'R', long)]
135        repo: Option<String>,
136        /// Skip `gh pr checkout` even if gh is available. By default
137        /// the working tree is left as-is — checkout is opt-in via
138        /// `--checkout` because dirty trees fail it loudly.
139        #[arg(long, default_value_t = false)]
140        checkout: bool,
141    },
142    /// Apply a patch file (or stdin) to the working tree
143    Apply(ApplyArgs),
144    /// Run the offline evaluation harness (no network/LLM calls)
145    Eval(EvalArgs),
146    /// Manage MCP servers
147    Mcp {
148        #[command(subcommand)]
149        command: McpCommand,
150    },
151    /// Execpolicy tooling
152    Execpolicy(ExecpolicyCommand),
153    /// Inspect feature flags
154    Features(FeaturesCli),
155    /// Run a command inside the sandbox
156    Sandbox(SandboxArgs),
157    /// Run a local server (e.g. MCP)
158    Serve(ServeArgs),
159    /// Resume a previous session by ID (use --last for most recent)
160    Resume {
161        /// Conversation/session id (UUID or prefix)
162        #[arg(value_name = "SESSION_ID")]
163        session_id: Option<String>,
164        /// Continue the most recent session in this workspace without a picker
165        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
166        last: bool,
167    },
168    /// Fork a previous session by ID (use --last for most recent)
169    Fork {
170        /// Conversation/session id (UUID or prefix)
171        #[arg(value_name = "SESSION_ID")]
172        session_id: Option<String>,
173        /// Fork the most recent session in this workspace without a picker
174        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
175        last: bool,
176    },
177}
178
179#[derive(Args, Debug, Clone)]
180pub struct ExecArgs {
181    /// Prompt to send to the model
182    pub prompt: String,
183    /// Override model for this run
184    #[arg(long)]
185    pub model: Option<String>,
186    /// Enable agentic mode with tool access and auto-approvals
187    #[arg(long, default_value_t = false)]
188    pub auto: bool,
189    /// Emit machine-readable JSON output
190    #[arg(long, default_value_t = false)]
191    pub json: bool,
192}
193
194#[derive(Args, Debug, Clone, Default)]
195pub struct SetupArgs {
196    /// Initialize MCP configuration at the configured path
197    #[arg(long, default_value_t = false)]
198    pub mcp: bool,
199    /// Initialize skills directory and an example skill
200    #[arg(long, default_value_t = false)]
201    pub skills: bool,
202    /// Initialize tools directory with a self-describing example script
203    #[arg(long, default_value_t = false)]
204    pub tools: bool,
205    /// Initialize plugins directory with a self-describing example
206    #[arg(long, default_value_t = false)]
207    pub plugins: bool,
208    /// Initialize MCP config, skills, tools, and plugins
209    #[arg(long, default_value_t = false)]
210    pub all: bool,
211    /// Create a local workspace skills directory (./skills)
212    #[arg(long, default_value_t = false)]
213    pub local: bool,
214    /// Overwrite existing template files
215    #[arg(long, default_value_t = false)]
216    pub force: bool,
217    /// Print a compact, read-only status report (no network calls)
218    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "clean"])]
219    pub status: bool,
220    /// Remove regenerable session checkpoints (latest + offline_queue)
221    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "status"])]
222    pub clean: bool,
223}
224
225#[derive(Args, Debug, Clone, Default)]
226pub struct DoctorArgs {
227    /// Emit machine-readable JSON output (skips live API connectivity check)
228    #[arg(long, default_value_t = false)]
229    pub json: bool,
230}
231
232#[derive(Args, Debug, Clone)]
233pub struct EvalArgs {
234    /// Intentionally fail a specific step (list, read, search, edit, patch, shell)
235    #[arg(long, value_name = "STEP")]
236    pub fail_step: Option<String>,
237    /// Shell command to run during the exec step
238    #[arg(long, default_value = "printf eval-harness")]
239    pub shell_command: String,
240    /// Token that must appear in shell output for validation
241    #[arg(long, default_value = "eval-harness")]
242    pub shell_expect_token: String,
243    /// Maximum characters stored per step output summary
244    #[arg(long, default_value_t = 240)]
245    pub max_output_chars: usize,
246    /// Emit machine-readable JSON output
247    #[arg(long, default_value_t = false)]
248    pub json: bool,
249    /// Append one JSONL fixture line per step to `<DIR>/<scenario>.jsonl`.
250    /// Mock LLM tests can later replay these fixtures.
251    #[arg(long, value_name = "DIR")]
252    pub record: Option<PathBuf>,
253}
254
255#[derive(Args, Debug, Clone, Default)]
256pub struct ModelsArgs {
257    /// Print models as pretty JSON
258    #[arg(long, default_value_t = false)]
259    pub json: bool,
260}
261
262#[derive(Args, Debug, Default, Clone)]
263pub struct FeatureToggles {
264    /// Enable a feature (repeatable). Equivalent to `features.<name>=true`.
265    #[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
266    pub enable: Vec<String>,
267
268    /// Disable a feature (repeatable). Equivalent to `features.<name>=false`.
269    #[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
270    pub disable: Vec<String>,
271}
272
273impl FeatureToggles {
274    pub fn apply(&self, config: &mut Config) -> Result<()> {
275        for feature in &self.enable {
276            config.set_feature(feature, true)?;
277        }
278        for feature in &self.disable {
279            config.set_feature(feature, false)?;
280        }
281        Ok(())
282    }
283}
284
285#[derive(Args, Debug, Clone)]
286pub struct ReviewArgs {
287    /// Review staged changes instead of the working tree
288    #[arg(long, conflicts_with = "base")]
289    pub staged: bool,
290    /// Base ref to diff against (e.g. origin/main)
291    #[arg(long)]
292    pub base: Option<String>,
293    /// Limit diff to a specific path
294    #[arg(long)]
295    pub path: Option<PathBuf>,
296    /// Override model for this review
297    #[arg(long)]
298    pub model: Option<String>,
299    /// Maximum diff characters to include
300    #[arg(long, default_value_t = 200_000)]
301    pub max_chars: usize,
302    /// Emit machine-readable JSON output
303    #[arg(long, default_value_t = false)]
304    pub json: bool,
305}
306
307#[derive(Args, Debug, Clone)]
308pub struct ApplyArgs {
309    /// Patch file to apply (defaults to stdin)
310    #[arg(value_name = "PATCH_FILE")]
311    pub patch_file: Option<PathBuf>,
312}
313
314#[derive(Args, Debug, Clone)]
315pub struct ServeArgs {
316    /// Start MCP server over stdio
317    #[arg(long)]
318    pub mcp: bool,
319    /// Start runtime HTTP/SSE API server
320    #[arg(long)]
321    pub http: bool,
322    /// Start ACP server over stdio for editor clients such as Zed
323    #[arg(long)]
324    pub acp: bool,
325    /// Bind host for HTTP server (default localhost)
326    #[arg(long, default_value = "127.0.0.1")]
327    pub host: String,
328    /// Bind port for HTTP server
329    #[arg(long, default_value_t = 7878)]
330    pub port: u16,
331    /// Background task worker count (1-16)
332    #[arg(long, default_value_t = 8)]
333    pub workers: usize,
334    /// Additional CORS origin to allow (repeatable). Stacks on top of the
335    /// built-in defaults (localhost:3000, localhost:1420, tauri://localhost).
336    /// Also reads `DEEPSEEK_CORS_ORIGINS` (comma-separated) and
337    /// `[runtime_api] cors_origins` from `config.toml`. Whalescale#255.
338    #[arg(long = "cors-origin", value_name = "URL")]
339    pub cors_origin: Vec<String>,
340    /// Require this bearer token for `/v1/*` runtime API routes. Also reads
341    /// `DEEPSEEK_RUNTIME_TOKEN` when omitted.
342    #[arg(long = "auth-token", value_name = "TOKEN")]
343    pub auth_token: Option<String>,
344}
345
346#[derive(Subcommand, Debug, Clone)]
347pub enum McpCommand {
348    /// List configured MCP servers
349    List,
350    /// Create a template MCP config at the configured path
351    Init {
352        /// Overwrite an existing MCP config file
353        #[arg(long, default_value_t = false)]
354        force: bool,
355    },
356    /// Connect to MCP servers and report status
357    Connect {
358        /// Optional server name to connect to
359        #[arg(value_name = "SERVER")]
360        server: Option<String>,
361    },
362    /// List tools discovered from MCP servers
363    Tools {
364        /// Optional server name to list tools for
365        #[arg(value_name = "SERVER")]
366        server: Option<String>,
367    },
368    /// Add an MCP server entry
369    Add {
370        /// Server name
371        name: String,
372        /// Command to launch stdio server
373        #[arg(long, conflicts_with = "url")]
374        command: Option<String>,
375        /// URL for streamable HTTP/SSE server
376        #[arg(long, conflicts_with = "command")]
377        url: Option<String>,
378        /// Arguments for command-based servers
379        #[arg(long = "arg")]
380        args: Vec<String>,
381    },
382    /// Remove an MCP server entry
383    Remove {
384        /// Server name
385        name: String,
386    },
387    /// Enable an MCP server
388    Enable {
389        /// Server name
390        name: String,
391    },
392    /// Disable an MCP server
393    Disable {
394        /// Server name
395        name: String,
396    },
397    /// Validate MCP config and required servers
398    Validate,
399    /// Register this DeepSeek binary as a local MCP stdio server.
400    ///
401    /// This adds a config entry that runs `deepseek serve --mcp` (stdio protocol).
402    /// For the HTTP/SSE runtime API, use `deepseek serve --http` directly instead.
403    #[command(
404        name = "add-self",
405        long_about = "Register this DeepSeek binary as a local MCP stdio server.\n\nAdds a config entry to ~/.deepseek/mcp.json that launches `deepseek serve --mcp`\nvia the stdio transport. Other DeepSeek sessions (or any MCP client) can then\ndiscover and call tools exposed by this server.\n\nUse `deepseek serve --http` instead if you need the HTTP/SSE runtime API."
406    )]
407    AddSelf {
408        /// Server name in mcp.json (default: "deepseek")
409        #[arg(long, default_value = "deepseek")]
410        name: String,
411        /// Workspace directory for the MCP server
412        #[arg(long)]
413        workspace: Option<String>,
414    },
415}
416
417#[derive(Args, Debug, Clone)]
418pub struct ExecpolicyCommand {
419    #[command(subcommand)]
420    pub command: ExecpolicySubcommand,
421}
422
423#[derive(Subcommand, Debug, Clone)]
424pub enum ExecpolicySubcommand {
425    /// Check execpolicy files against a command
426    Check(crate::execpolicy::ExecPolicyCheckCommand),
427}
428
429#[derive(Args, Debug, Clone)]
430pub struct FeaturesCli {
431    #[command(subcommand)]
432    pub command: FeaturesSubcommand,
433}
434
435#[derive(Subcommand, Debug, Clone)]
436pub enum FeaturesSubcommand {
437    /// List known feature flags and their state
438    List,
439}
440
441#[derive(Args, Debug, Clone)]
442pub struct SandboxArgs {
443    #[command(subcommand)]
444    pub command: SandboxCommand,
445}
446
447#[derive(Subcommand, Debug, Clone)]
448pub enum SandboxCommand {
449    /// Run a command with sandboxing
450    Run {
451        /// Sandbox policy (danger-full-access, read-only, external-sandbox, workspace-write)
452        #[arg(long, default_value = "workspace-write")]
453        policy: String,
454        /// Allow outbound network access
455        #[arg(long)]
456        network: bool,
457        /// Additional writable roots (repeatable)
458        #[arg(long, value_name = "PATH")]
459        writable_root: Vec<PathBuf>,
460        /// Exclude TMPDIR from writable paths
461        #[arg(long)]
462        exclude_tmpdir: bool,
463        /// Exclude /tmp from writable paths
464        #[arg(long)]
465        exclude_slash_tmp: bool,
466        /// Command working directory
467        #[arg(long)]
468        cwd: Option<PathBuf>,
469        /// Timeout in milliseconds
470        #[arg(long, default_value_t = 60_000)]
471        timeout_ms: u64,
472        /// Command and arguments to run
473        #[arg(required = true, trailing_var_arg = true)]
474        command: Vec<String>,
475    },
476}