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    /// Layer-2 cross-platform completion gate check (replaces PowerShell scripts)
147    CoverageGate(CoverageGateArgs),
148    /// Manage MCP servers
149    Mcp {
150        #[command(subcommand)]
151        command: McpCommand,
152    },
153    /// Execpolicy tooling
154    Execpolicy(ExecpolicyCommand),
155    /// Inspect feature flags
156    Features(FeaturesCli),
157    /// Run a command inside the sandbox
158    Sandbox(SandboxArgs),
159    /// Run a local server (e.g. MCP)
160    Serve(ServeArgs),
161    /// Resume a previous session by ID (use --last for most recent)
162    Resume {
163        /// Conversation/session id (UUID or prefix)
164        #[arg(value_name = "SESSION_ID")]
165        session_id: Option<String>,
166        /// Continue the most recent session in this workspace without a picker
167        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
168        last: bool,
169    },
170    /// Fork a previous session by ID (use --last for most recent)
171    Fork {
172        /// Conversation/session id (UUID or prefix)
173        #[arg(value_name = "SESSION_ID")]
174        session_id: Option<String>,
175        /// Fork the most recent session in this workspace without a picker
176        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
177        last: bool,
178    },
179}
180
181#[derive(Args, Debug, Clone)]
182pub struct ExecArgs {
183    /// Prompt to send to the model
184    pub prompt: String,
185    /// Override model for this run
186    #[arg(long)]
187    pub model: Option<String>,
188    /// Enable agentic mode with tool access and auto-approvals
189    #[arg(long, default_value_t = false)]
190    pub auto: bool,
191    /// Emit machine-readable JSON output
192    #[arg(long, default_value_t = false)]
193    pub json: bool,
194}
195
196#[derive(Args, Debug, Clone, Default)]
197pub struct SetupArgs {
198    /// Initialize MCP configuration at the configured path
199    #[arg(long, default_value_t = false)]
200    pub mcp: bool,
201    /// Initialize skills directory and an example skill
202    #[arg(long, default_value_t = false)]
203    pub skills: bool,
204    /// Initialize tools directory with a self-describing example script
205    #[arg(long, default_value_t = false)]
206    pub tools: bool,
207    /// Initialize plugins directory with a self-describing example
208    #[arg(long, default_value_t = false)]
209    pub plugins: bool,
210    /// Initialize MCP config, skills, tools, and plugins
211    #[arg(long, default_value_t = false)]
212    pub all: bool,
213    /// Create a local workspace skills directory (./skills)
214    #[arg(long, default_value_t = false)]
215    pub local: bool,
216    /// Overwrite existing template files
217    #[arg(long, default_value_t = false)]
218    pub force: bool,
219    /// Print a compact, read-only status report (no network calls)
220    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "clean"])]
221    pub status: bool,
222    /// Remove regenerable session checkpoints (latest + offline_queue)
223    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "status"])]
224    pub clean: bool,
225}
226
227#[derive(Args, Debug, Clone, Default)]
228pub struct DoctorArgs {
229    /// Emit machine-readable JSON output (skips live API connectivity check)
230    #[arg(long, default_value_t = false)]
231    pub json: bool,
232}
233
234#[derive(Args, Debug, Clone)]
235pub struct EvalArgs {
236    /// Intentionally fail a specific step (list, read, search, edit, patch, shell)
237    #[arg(long, value_name = "STEP")]
238    pub fail_step: Option<String>,
239    /// Shell command to run during the exec step
240    #[arg(long, default_value = "printf eval-harness")]
241    pub shell_command: String,
242    /// Token that must appear in shell output for validation
243    #[arg(long, default_value = "eval-harness")]
244    pub shell_expect_token: String,
245    /// Maximum characters stored per step output summary
246    #[arg(long, default_value_t = 240)]
247    pub max_output_chars: usize,
248    /// Emit machine-readable JSON output
249    #[arg(long, default_value_t = false)]
250    pub json: bool,
251    /// Append one JSONL fixture line per step to `<DIR>/<scenario>.jsonl`.
252    /// Mock LLM tests can later replay these fixtures.
253    #[arg(long, value_name = "DIR")]
254    pub record: Option<PathBuf>,
255}
256
257#[derive(Args, Debug, Clone, Default)]
258pub struct ModelsArgs {
259    /// Print models as pretty JSON
260    #[arg(long, default_value_t = false)]
261    pub json: bool,
262}
263
264#[derive(Args, Debug, Default, Clone)]
265pub struct FeatureToggles {
266    /// Enable a feature (repeatable). Equivalent to `features.<name>=true`.
267    #[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
268    pub enable: Vec<String>,
269
270    /// Disable a feature (repeatable). Equivalent to `features.<name>=false`.
271    #[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
272    pub disable: Vec<String>,
273}
274
275impl FeatureToggles {
276    pub fn apply(&self, config: &mut Config) -> Result<()> {
277        for feature in &self.enable {
278            config.set_feature(feature, true)?;
279        }
280        for feature in &self.disable {
281            config.set_feature(feature, false)?;
282        }
283        Ok(())
284    }
285}
286
287#[derive(Args, Debug, Clone)]
288pub struct ReviewArgs {
289    /// Review staged changes instead of the working tree
290    #[arg(long, conflicts_with = "base")]
291    pub staged: bool,
292    /// Base ref to diff against (e.g. origin/main)
293    #[arg(long)]
294    pub base: Option<String>,
295    /// Limit diff to a specific path
296    #[arg(long)]
297    pub path: Option<PathBuf>,
298    /// Override model for this review
299    #[arg(long)]
300    pub model: Option<String>,
301    /// Maximum diff characters to include
302    #[arg(long, default_value_t = 200_000)]
303    pub max_chars: usize,
304    /// Emit machine-readable JSON output
305    #[arg(long, default_value_t = false)]
306    pub json: bool,
307}
308
309#[derive(Args, Debug, Clone)]
310pub struct ApplyArgs {
311    /// Patch file to apply (defaults to stdin)
312    #[arg(value_name = "PATCH_FILE")]
313    pub patch_file: Option<PathBuf>,
314}
315
316#[derive(Args, Debug, Clone)]
317pub struct ServeArgs {
318    /// Start MCP server over stdio
319    #[arg(long)]
320    pub mcp: bool,
321    /// Start runtime HTTP/SSE API server
322    #[arg(long)]
323    pub http: bool,
324    /// Start ACP server over stdio for editor clients such as Zed
325    #[arg(long)]
326    pub acp: bool,
327    /// Bind host for HTTP server (default localhost)
328    #[arg(long, default_value = "127.0.0.1")]
329    pub host: String,
330    /// Bind port for HTTP server
331    #[arg(long, default_value_t = 7878)]
332    pub port: u16,
333    /// Background task worker count (1-16)
334    #[arg(long, default_value_t = 8)]
335    pub workers: usize,
336    /// Additional CORS origin to allow (repeatable). Stacks on top of the
337    /// built-in defaults (localhost:3000, localhost:1420, tauri://localhost).
338    /// Also reads `DEEPSEEK_CORS_ORIGINS` (comma-separated) and
339    /// `[runtime_api] cors_origins` from `config.toml`. Whalescale#255.
340    #[arg(long = "cors-origin", value_name = "URL")]
341    pub cors_origin: Vec<String>,
342    /// Require this bearer token for `/v1/*` runtime API routes. Also reads
343    /// `DEEPSEEK_RUNTIME_TOKEN` when omitted.
344    #[arg(long = "auth-token", value_name = "TOKEN")]
345    pub auth_token: Option<String>,
346}
347
348#[derive(Subcommand, Debug, Clone)]
349pub enum McpCommand {
350    /// List configured MCP servers
351    List,
352    /// Create a template MCP config at the configured path
353    Init {
354        /// Overwrite an existing MCP config file
355        #[arg(long, default_value_t = false)]
356        force: bool,
357    },
358    /// Connect to MCP servers and report status
359    Connect {
360        /// Optional server name to connect to
361        #[arg(value_name = "SERVER")]
362        server: Option<String>,
363    },
364    /// List tools discovered from MCP servers
365    Tools {
366        /// Optional server name to list tools for
367        #[arg(value_name = "SERVER")]
368        server: Option<String>,
369    },
370    /// Add an MCP server entry
371    Add {
372        /// Server name
373        name: String,
374        /// Command to launch stdio server
375        #[arg(long, conflicts_with = "url")]
376        command: Option<String>,
377        /// URL for streamable HTTP/SSE server
378        #[arg(long, conflicts_with = "command")]
379        url: Option<String>,
380        /// Arguments for command-based servers
381        #[arg(long = "arg")]
382        args: Vec<String>,
383    },
384    /// Remove an MCP server entry
385    Remove {
386        /// Server name
387        name: String,
388    },
389    /// Enable an MCP server
390    Enable {
391        /// Server name
392        name: String,
393    },
394    /// Disable an MCP server
395    Disable {
396        /// Server name
397        name: String,
398    },
399    /// Validate MCP config and required servers
400    Validate,
401    /// Register this DeepSeek binary as a local MCP stdio server.
402    ///
403    /// This adds a config entry that runs `deepseek serve --mcp` (stdio protocol).
404    /// For the HTTP/SSE runtime API, use `deepseek serve --http` directly instead.
405    #[command(
406        name = "add-self",
407        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."
408    )]
409    AddSelf {
410        /// Server name in mcp.json (default: "deepseek")
411        #[arg(long, default_value = "deepseek")]
412        name: String,
413        /// Workspace directory for the MCP server
414        #[arg(long)]
415        workspace: Option<String>,
416    },
417}
418
419#[derive(Args, Debug, Clone)]
420pub struct ExecpolicyCommand {
421    #[command(subcommand)]
422    pub command: ExecpolicySubcommand,
423}
424
425#[derive(Subcommand, Debug, Clone)]
426pub enum ExecpolicySubcommand {
427    /// Check execpolicy files against a command
428    Check(crate::execpolicy::ExecPolicyCheckCommand),
429}
430
431#[derive(Args, Debug, Clone)]
432pub struct FeaturesCli {
433    #[command(subcommand)]
434    pub command: FeaturesSubcommand,
435}
436
437#[derive(Subcommand, Debug, Clone)]
438pub enum FeaturesSubcommand {
439    /// List known feature flags and their state
440    List,
441}
442
443#[derive(Args, Debug, Clone)]
444pub struct SandboxArgs {
445    #[command(subcommand)]
446    pub command: SandboxCommand,
447}
448
449#[derive(Subcommand, Debug, Clone)]
450pub enum SandboxCommand {
451    /// Gate G0 PoC subcommands (Windows only)
452    Poc {
453        #[command(subcommand)]
454        command: SandboxPocCommand,
455    },
456    /// Remove unelevated sandbox ACL state (Phase 1; no WFP/users)
457    Teardown {
458        /// Keep cap_sid file and sandbox logs
459        #[arg(long)]
460        keep_logs: bool,
461    },
462    /// Elevated Windows sandbox setup (UAC; creates sandbox users + marker)
463    Setup,
464    /// Grant an additional read path for elevated sandbox users (PR-3.3)
465    AddReadDir {
466        /// Directory or file to grant read (+execute) access
467        path: PathBuf,
468    },
469    /// Run a command with sandboxing
470    Run {
471        /// Sandbox policy (danger-full-access, read-only, external-sandbox, workspace-write)
472        #[arg(long, default_value = "workspace-write")]
473        policy: String,
474        /// Allow outbound network access
475        #[arg(long)]
476        network: bool,
477        /// Additional writable roots (repeatable)
478        #[arg(long, value_name = "PATH")]
479        writable_root: Vec<PathBuf>,
480        /// Exclude TMPDIR from writable paths
481        #[arg(long)]
482        exclude_tmpdir: bool,
483        /// Exclude /tmp from writable paths
484        #[arg(long)]
485        exclude_slash_tmp: bool,
486        /// Command working directory
487        #[arg(long)]
488        cwd: Option<PathBuf>,
489        /// Timeout in milliseconds
490        #[arg(long, default_value_t = 60_000)]
491        timeout_ms: u64,
492        /// Command and arguments to run
493        #[arg(required = true, trailing_var_arg = true)]
494        command: Vec<String>,
495    },
496}
497
498#[derive(Subcommand, Debug, Clone)]
499pub enum SandboxPocCommand {
500    /// Verify unelevated deny-read isolation; writes ~/.zagens/.sandbox/unelevated_deny_read_poc.json
501    DenyRead,
502}
503
504/// Arguments for `zagens coverage-gate`
505#[derive(Args, Debug, Clone)]
506pub struct CoverageGateArgs {
507    /// Workspace directory (default: current directory)
508    #[arg(short, long)]
509    pub workspace: Option<std::path::PathBuf>,
510    /// Require all todo-list items to be marked completed
511    #[arg(long, default_value_t = true)]
512    pub require_checklist_complete: bool,
513    /// Run `cargo test` to verify test suite passes (slow; off by default)
514    #[arg(long = "run-tests", default_value_t = false)]
515    pub run_tests: bool,
516    /// Emit machine-readable JSON output instead of human-readable text
517    #[arg(long, default_value_t = false)]
518    pub json: bool,
519    /// Task ID to check in the CRAFT blackboard (optional; checks latest if omitted)
520    #[arg(long)]
521    pub task_id: Option<String>,
522    /// Exit 0 even when gate fails (report-only mode)
523    #[arg(long, default_value_t = false)]
524    pub no_fail: bool,
525}