Skip to main content

rho_coding_agent/
cli.rs

1use std::{num::NonZeroUsize, path::PathBuf, time::Duration};
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5use rho_providers::{credentials::CredentialStoreBackend, reasoning::ReasoningLevel};
6
7use crate::{
8    app::automation_protocol::parse_duration,
9    permission::{PermissionMode, PermissionModeParseError},
10};
11
12fn parse_permission_mode(value: &str) -> Result<PermissionMode, PermissionModeParseError> {
13    value.parse()
14}
15
16fn parse_credential_store_backend(value: &str) -> Result<CredentialStoreBackend, String> {
17    CredentialStoreBackend::parse(value).map_err(|error| error.to_string())
18}
19
20fn parse_auth_profile(value: &str) -> Result<String, String> {
21    let profiles = rho_providers::auth_profiles();
22    if profiles.contains(&value) {
23        return Ok(value.to_string());
24    }
25    Err(format!(
26        "invalid value '{value}' for '--auth'; expected one of: {}",
27        profiles.join(", ")
28    ))
29}
30
31/// Output contract used by a non-interactive `rho run` invocation.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
33pub enum OutputFormat {
34    /// Print only the authoritative final assistant answer.
35    #[default]
36    Text,
37    /// Stream independently versioned JSON Lines events.
38    Jsonl,
39}
40
41/// Output contract for workflow plans and snapshots.
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
43pub enum WorkflowDocumentFormat {
44    /// Print a human-readable document.
45    #[default]
46    Text,
47    /// Print one JSON document.
48    Json,
49}
50
51/// Output contract for workflow execution.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
53pub enum WorkflowRunFormat {
54    /// Print human-readable state changes.
55    Text,
56    /// Stream versioned JSON Lines events.
57    Jsonl,
58}
59
60#[derive(Parser, Debug)]
61#[command(name = "rho")]
62pub struct Cli {
63    #[arg(long)]
64    pub provider: Option<String>,
65    #[arg(long)]
66    pub model: Option<String>,
67    #[arg(long)]
68    pub config: Option<PathBuf>,
69    #[arg(long, value_parser = parse_auth_profile)]
70    pub auth: Option<String>,
71    /// Do not send rho's system prompt, including AGENTS.md and skill context.
72    #[arg(long)]
73    pub no_system_prompt: bool,
74    /// Do not expose any tools to the model.
75    #[arg(long)]
76    pub no_tools: bool,
77    /// Do not expose the delegated-agent tools (agent/agents) to the model.
78    #[arg(long, global = true)]
79    pub no_subagents: bool,
80    /// Select the agent definition used for this session or automation run.
81    #[arg(long, global = true, value_name = "ID")]
82    pub agent: Option<String>,
83    /// Override reasoning level: off, minimal, low, medium, high, xhigh, or max.
84    #[arg(long)]
85    pub reasoning: Option<ReasoningLevel>,
86    /// Override permission mode: bypass, auto, plan, or supervised.
87    #[arg(long, value_name = "MODE", value_parser = parse_permission_mode)]
88    pub(crate) permission_mode: Option<PermissionMode>,
89    /// Persist --provider/--model/--auth/--reasoning overrides to the config file.
90    ///
91    /// Without this flag, those overrides apply only to the current invocation.
92    #[arg(long)]
93    pub save: bool,
94    /// Resume an existing session by UUID or UUID prefix. Omit the ID to choose from a picker.
95    #[arg(short = 'R', long, value_name = "ID", num_args = 0..=1)]
96    pub resume: Option<Option<String>>,
97    #[command(subcommand)]
98    pub command: Option<Command>,
99}
100
101#[derive(Subcommand, Debug)]
102pub enum Command {
103    /// Run one non-interactive automation prompt and print the final answer.
104    Run {
105        /// Read additional prompt text from stdin.
106        ///
107        /// Required when stdin is a pipe or redirected file. Without this flag,
108        /// redirected stdin is rejected so prompt text is not silently dropped.
109        #[arg(long)]
110        stdin: bool,
111        /// Write a structured status/result file (JSON) that is updated during
112        /// the run and finalized on exit. With `--output text` (the default),
113        /// progress and streamed assistant text go to stdout and the run ends
114        /// with a completion marker; the result file is the durable final
115        /// answer. With `--output jsonl`, stdout stays the JSONL event stream.
116        #[arg(long, value_name = "PATH")]
117        output_file: Option<PathBuf>,
118        /// Select plain final-answer output or a JSON Lines event stream.
119        #[arg(long, value_enum, default_value_t)]
120        output: OutputFormat,
121        /// Override the model-step budget for this run.
122        #[arg(long, value_name = "N")]
123        max_steps: Option<NonZeroUsize>,
124        /// Cancel the run after this wall-clock duration.
125        #[arg(long, value_name = "DURATION", value_parser = parse_duration)]
126        timeout: Option<Duration>,
127        /// Prompt text to send to the agent.
128        #[arg(value_name = "PROMPT", num_args = 0..)]
129        prompt: Vec<String>,
130    },
131    /// Watch a delegated agent run in a read-only TUI.
132    Attach {
133        /// Delegated run ID shown when the agent was started.
134        #[arg(value_name = "ID")]
135        id: String,
136    },
137    /// Log in to a provider from a browser or device-code flow.
138    Login {
139        /// Provider to authenticate, for example openai-codex or github-copilot.
140        #[arg(value_name = "PROVIDER")]
141        provider: String,
142        /// Use device-code login instead of opening a local browser callback.
143        #[arg(long)]
144        device_auth: bool,
145    },
146    /// Configure or probe provider credential storage.
147    CredentialStore {
148        #[command(subcommand)]
149        command: CredentialStoreCommand,
150    },
151    /// Update rho using the detected installation method.
152    Update,
153    /// List, rename, or delete saved sessions.
154    Sessions {
155        #[command(subcommand)]
156        command: SessionsCommand,
157    },
158    /// Inspect configured Model Context Protocol servers.
159    Mcp {
160        #[command(subcommand)]
161        command: McpCommand,
162    },
163    /// List, inspect, install, and activate local Agent Plugin packages.
164    Plugins {
165        #[command(subcommand)]
166        command: PluginsCommand,
167    },
168    /// Validate, plan, run, and inspect deterministic workflows.
169    Workflow {
170        #[command(subcommand)]
171        command: WorkflowCommand,
172    },
173    /// Internal supervised workflow planner worker. Not a public command.
174    #[command(name = "__workflow_planner_worker", hide = true)]
175    WorkflowPlannerWorker,
176}
177
178/// Argv entry for the internal supervised planner worker process.
179pub const WORKFLOW_PLANNER_WORKER_COMMAND: &str = "__workflow_planner_worker";
180
181#[derive(Subcommand, Debug)]
182pub enum WorkflowCommand {
183    /// List saved workflow plans and runs for the current workspace.
184    List {
185        /// Include only saved plans.
186        #[arg(long, conflicts_with = "runs")]
187        plans: bool,
188        /// Include only runs.
189        #[arg(long, conflicts_with = "plans")]
190        runs: bool,
191        /// Limit how many rows to print per section.
192        #[arg(long, value_name = "N")]
193        limit: Option<NonZeroUsize>,
194        /// Print one JSON document instead of text rows.
195        #[arg(long)]
196        json: bool,
197    },
198    /// Validate a Starlark workflow without creating durable state.
199    Validate {
200        /// Workflow entry file under the current workspace.
201        #[arg(value_name = "FILE")]
202        file: PathBuf,
203        /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
204        #[arg(long, value_name = "KEY=JSON")]
205        input: Vec<String>,
206    },
207    /// Validate, freeze, and persist an immutable workflow plan.
208    Plan {
209        /// Workflow entry file under the current workspace.
210        #[arg(value_name = "FILE")]
211        file: PathBuf,
212        /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
213        #[arg(long, value_name = "KEY=JSON")]
214        input: Vec<String>,
215        /// Select human-readable or machine-readable plan output.
216        #[arg(long, value_enum, default_value_t)]
217        output: WorkflowDocumentFormat,
218    },
219    /// Create and run a workflow from an immutable plan.
220    Run {
221        /// Full plan UUID or unique UUID prefix.
222        #[arg(value_name = "PLAN_ID")]
223        plan_id: String,
224        /// Confirm the exact plan digest without an interactive prompt.
225        #[arg(long)]
226        yes: bool,
227        /// Select text or JSON Lines instead of the workflow TUI.
228        #[arg(long, value_enum)]
229        output: Option<WorkflowRunFormat>,
230    },
231    /// Read one durable workflow run snapshot.
232    Status {
233        /// Full run UUID or unique UUID prefix.
234        #[arg(value_name = "RUN_ID")]
235        run_id: String,
236        /// Select human-readable or machine-readable snapshot output.
237        #[arg(long, value_enum, default_value_t)]
238        output: WorkflowDocumentFormat,
239    },
240    /// Request cancellation of a durable workflow run.
241    Cancel {
242        /// Full run UUID or unique UUID prefix.
243        #[arg(value_name = "RUN_ID")]
244        run_id: String,
245    },
246    /// Resume a durable workflow run from its frozen graph.
247    Resume {
248        /// Full run UUID or unique UUID prefix.
249        #[arg(value_name = "RUN_ID")]
250        run_id: String,
251        /// Confirm the frozen graph without an interactive prompt.
252        #[arg(long)]
253        yes: bool,
254        /// Confirm that no prior process remains and relaunch uncertain attempts.
255        #[arg(long)]
256        recover_uncertain: bool,
257        /// Select text or JSON Lines instead of the workflow TUI.
258        #[arg(long, value_enum)]
259        output: Option<WorkflowRunFormat>,
260    },
261}
262
263#[derive(Subcommand, Debug)]
264pub enum SessionsCommand {
265    /// List saved sessions for the current workspace (or all projects).
266    List {
267        /// Include sessions from every workspace, not only the current directory.
268        #[arg(long)]
269        all_projects: bool,
270        /// Case-insensitive filter over id, title, and first user message.
271        #[arg(long, short = 'q', value_name = "TEXT")]
272        search: Option<String>,
273        /// Limit how many sessions to print.
274        #[arg(long, value_name = "N")]
275        limit: Option<NonZeroUsize>,
276        /// Print one JSON document instead of text rows.
277        #[arg(long)]
278        json: bool,
279    },
280    /// Export a saved session transcript by UUID or UUID prefix.
281    Export {
282        /// Session UUID or unique prefix.
283        #[arg(value_name = "ID")]
284        id_prefix: String,
285        /// Output path. Omit to write under ~/.rho/exports/.
286        #[arg(long, short = 'o', value_name = "PATH")]
287        output: Option<PathBuf>,
288        /// Explicit format. When omitted, the path extension selects html, md, or json.
289        #[arg(long, value_enum)]
290        format: Option<crate::export::ExportFormat>,
291        /// Overwrite an existing file.
292        #[arg(long)]
293        force: bool,
294    },
295    /// Delete one or more sessions by UUID or UUID prefix.
296    Rm {
297        /// Session UUID or unique prefix. May be repeated.
298        #[arg(value_name = "ID", required = true, num_args = 1..)]
299        ids: Vec<String>,
300        /// Delete even when a parent-linked run is still non-terminal.
301        ///
302        /// Use only for stale Starting/Running artifacts left after a crash.
303        #[arg(long)]
304        force: bool,
305        /// Skip the confirmation prompt for cross-project deletes.
306        #[arg(short = 'y', long)]
307        yes: bool,
308    },
309    /// Delete sessions whose workspace directories no longer exist.
310    Cleanup {
311        /// Delete even when a parent-linked run is still non-terminal.
312        ///
313        /// Use only for stale Starting/Running artifacts left after a crash.
314        #[arg(long)]
315        force: bool,
316        /// Skip the confirmation prompt.
317        #[arg(short = 'y', long)]
318        yes: bool,
319    },
320    /// Rename a session by UUID or UUID prefix.
321    Rename {
322        /// Session UUID or unique prefix.
323        #[arg(value_name = "ID")]
324        id_prefix: String,
325        /// New session title. Multiple words are joined with spaces.
326        #[arg(value_name = "TITLE", required = true, num_args = 1.., trailing_var_arg = true)]
327        title: Vec<String>,
328    },
329}
330
331#[derive(Subcommand, Debug)]
332pub enum McpCommand {
333    /// List configured MCP servers from the selected config and plugins.
334    List {
335        /// Print one JSON document instead of text rows.
336        #[arg(long)]
337        json: bool,
338        /// Start enabled servers and report live connection status.
339        #[arg(long)]
340        connect: bool,
341    },
342    /// Show one MCP server by identity.
343    Show {
344        /// Server table key from `[mcp.servers.<id>]`.
345        #[arg(value_name = "ID")]
346        id: String,
347        /// Print one JSON document instead of text.
348        #[arg(long)]
349        json: bool,
350        /// Start enabled servers and report live connection status.
351        #[arg(long)]
352        connect: bool,
353    },
354}
355
356/// Target scope for plugin install and link.
357#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
358pub enum PluginsScope {
359    /// User root: `~/.agents/plugins`.
360    #[default]
361    User,
362    /// Project root: `<repository>/.agents/plugins`.
363    Project,
364}
365
366#[derive(Subcommand, Debug)]
367pub enum PluginsCommand {
368    /// List discovered Agent Plugin packages.
369    List {
370        /// Print one JSON document instead of text rows.
371        #[arg(long)]
372        json: bool,
373    },
374    /// Inspect one plugin by package name without executing package code.
375    Inspect {
376        /// Plugin package name from `plugin.json`.
377        #[arg(value_name = "NAME")]
378        name: String,
379        /// Print one JSON document instead of text.
380        #[arg(long)]
381        json: bool,
382    },
383    /// Copy a local plugin package into a managed plugins root.
384    Install {
385        /// Path to a directory that contains `plugin.json`.
386        #[arg(value_name = "PATH")]
387        path: PathBuf,
388        /// Install under the user or project plugins root.
389        #[arg(long, value_enum, default_value_t = PluginsScope::User)]
390        scope: PluginsScope,
391        /// Replace an existing package at the destination.
392        #[arg(long)]
393        force: bool,
394    },
395    /// Symlink a local plugin package into a managed plugins root.
396    Link {
397        /// Path to a directory that contains `plugin.json`.
398        #[arg(value_name = "PATH")]
399        path: PathBuf,
400        /// Link under the user or project plugins root.
401        #[arg(long, value_enum, default_value_t = PluginsScope::User)]
402        scope: PluginsScope,
403        /// Replace an existing package at the destination.
404        #[arg(long)]
405        force: bool,
406    },
407    /// Enable a discovered plugin for new sessions.
408    Enable {
409        /// Plugin package name from `plugin.json`.
410        #[arg(value_name = "NAME")]
411        name: String,
412    },
413    /// Disable a discovered plugin without deleting package files.
414    Disable {
415        /// Plugin package name from `plugin.json`.
416        #[arg(value_name = "NAME")]
417        name: String,
418    },
419    /// Remove an installed or linked package from a managed plugins root.
420    Remove {
421        /// Plugin package name from `plugin.json`.
422        #[arg(value_name = "NAME")]
423        name: String,
424        /// Skip the confirmation prompt.
425        #[arg(short = 'y', long)]
426        yes: bool,
427    },
428}
429
430#[derive(Subcommand, Debug)]
431pub enum CredentialStoreCommand {
432    /// Test a credential backend by writing and deleting a temporary secret.
433    Probe {
434        /// Backend to test: os or file (`auto` is accepted as an alias for os).
435        #[arg(
436            value_name = "BACKEND",
437            default_value = "os",
438            value_parser = parse_credential_store_backend
439        )]
440        backend: CredentialStoreBackend,
441    },
442    /// Show the configured credential backend (unset, os, or file).
443    Status,
444    /// Save the credential backend used by future rho processes.
445    Set {
446        /// Backend to use: os or file (`auto` is accepted as an alias for os).
447        #[arg(value_name = "BACKEND", value_parser = parse_credential_store_backend)]
448        backend: CredentialStoreBackend,
449    },
450}
451
452#[cfg(test)]
453#[path = "cli_tests.rs"]
454mod tests;