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::app::automation_protocol::parse_duration;
8
9fn parse_credential_store_backend(value: &str) -> Result<CredentialStoreBackend, String> {
10    CredentialStoreBackend::parse(value).map_err(|error| error.to_string())
11}
12
13fn parse_auth_profile(value: &str) -> Result<String, String> {
14    let profiles = rho_providers::auth_profiles();
15    if profiles.contains(&value) {
16        return Ok(value.to_string());
17    }
18    Err(format!(
19        "invalid value '{value}' for '--auth'; expected one of: {}",
20        profiles.join(", ")
21    ))
22}
23
24/// Output contract used by a non-interactive `rho run` invocation.
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
26pub enum OutputFormat {
27    /// Print only the authoritative final assistant answer.
28    #[default]
29    Text,
30    /// Stream independently versioned JSON Lines events.
31    Jsonl,
32}
33
34/// Output contract for workflow plans and snapshots.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
36pub enum WorkflowDocumentFormat {
37    /// Print a human-readable document.
38    #[default]
39    Text,
40    /// Print one JSON document.
41    Json,
42}
43
44/// Output contract for workflow execution.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
46pub enum WorkflowRunFormat {
47    /// Print human-readable state changes.
48    Text,
49    /// Stream versioned JSON Lines events.
50    Jsonl,
51}
52
53#[derive(Parser, Debug)]
54#[command(name = "rho")]
55pub struct Cli {
56    #[arg(long)]
57    pub provider: Option<String>,
58    #[arg(long)]
59    pub model: Option<String>,
60    #[arg(long)]
61    pub config: Option<PathBuf>,
62    #[arg(long, value_parser = parse_auth_profile)]
63    pub auth: Option<String>,
64    /// Do not send rho's system prompt, including AGENTS.md and skill context.
65    #[arg(long)]
66    pub no_system_prompt: bool,
67    /// Do not expose any tools to the model.
68    #[arg(long)]
69    pub no_tools: bool,
70    /// Do not expose the delegated-agent tools (agent/agents) to the model.
71    #[arg(long, global = true)]
72    pub no_subagents: bool,
73    /// Select the agent definition used for this session or automation run.
74    #[arg(long, global = true, value_name = "ID")]
75    pub agent: Option<String>,
76    /// Override reasoning level: off, minimal, low, medium, high, xhigh, or max.
77    #[arg(long)]
78    pub reasoning: Option<ReasoningLevel>,
79    /// Resume an existing session by UUID or UUID prefix. Omit the ID to choose from a picker.
80    #[arg(short = 'R', long, value_name = "ID", num_args = 0..=1)]
81    pub resume: Option<Option<String>>,
82    #[command(subcommand)]
83    pub command: Option<Command>,
84}
85
86#[derive(Subcommand, Debug)]
87pub enum Command {
88    /// Run one non-interactive automation prompt and print the final answer.
89    Run {
90        /// Read additional prompt text from stdin.
91        #[arg(long)]
92        stdin: bool,
93        /// Stream progress to stdout and write a structured status/result
94        /// file (JSON) that is updated during the run and finalized on exit.
95        #[arg(long, value_name = "PATH")]
96        output_file: Option<PathBuf>,
97        /// Select plain final-answer output or a JSON Lines event stream.
98        #[arg(long, value_enum, default_value_t)]
99        output: OutputFormat,
100        /// Override the model-step budget for this run.
101        #[arg(long, value_name = "N")]
102        max_steps: Option<NonZeroUsize>,
103        /// Cancel the run after this wall-clock duration.
104        #[arg(long, value_name = "DURATION", value_parser = parse_duration)]
105        timeout: Option<Duration>,
106        /// Prompt text to send to the agent.
107        #[arg(value_name = "PROMPT", num_args = 0..)]
108        prompt: Vec<String>,
109    },
110    /// Watch a delegated agent run in a read-only TUI.
111    Attach {
112        /// Delegated run ID shown when the agent was started.
113        #[arg(value_name = "ID")]
114        id: String,
115    },
116    /// Log in to a provider from a browser or device-code flow.
117    Login {
118        /// Provider to authenticate, for example openai-codex or github-copilot.
119        #[arg(value_name = "PROVIDER")]
120        provider: String,
121        /// Use device-code login instead of opening a local browser callback.
122        #[arg(long)]
123        device_auth: bool,
124    },
125    /// Configure or probe provider credential storage.
126    CredentialStore {
127        #[command(subcommand)]
128        command: CredentialStoreCommand,
129    },
130    /// Update rho using the detected installation method.
131    Update,
132    /// List, rename, or delete saved sessions.
133    Sessions {
134        #[command(subcommand)]
135        command: SessionsCommand,
136    },
137    /// Validate, plan, run, and inspect deterministic workflows.
138    Workflow {
139        #[command(subcommand)]
140        command: WorkflowCommand,
141    },
142    /// Internal supervised workflow planner worker. Not a public command.
143    #[command(name = "__workflow_planner_worker", hide = true)]
144    WorkflowPlannerWorker,
145}
146
147/// Argv entry for the internal supervised planner worker process.
148pub const WORKFLOW_PLANNER_WORKER_COMMAND: &str = "__workflow_planner_worker";
149
150#[derive(Subcommand, Debug)]
151pub enum WorkflowCommand {
152    /// Validate a Starlark workflow without creating durable state.
153    Validate {
154        /// Workflow entry file under the current workspace.
155        #[arg(value_name = "FILE")]
156        file: PathBuf,
157        /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
158        #[arg(long, value_name = "KEY=JSON")]
159        input: Vec<String>,
160    },
161    /// Validate, freeze, and persist an immutable workflow plan.
162    Plan {
163        /// Workflow entry file under the current workspace.
164        #[arg(value_name = "FILE")]
165        file: PathBuf,
166        /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
167        #[arg(long, value_name = "KEY=JSON")]
168        input: Vec<String>,
169        /// Select human-readable or machine-readable plan output.
170        #[arg(long, value_enum, default_value_t)]
171        output: WorkflowDocumentFormat,
172    },
173    /// Create and run a workflow from an immutable plan.
174    Run {
175        /// Full plan UUID or unique UUID prefix.
176        #[arg(value_name = "PLAN_ID")]
177        plan_id: String,
178        /// Confirm the exact plan digest without an interactive prompt.
179        #[arg(long)]
180        yes: bool,
181        /// Select text or JSON Lines instead of the workflow TUI.
182        #[arg(long, value_enum)]
183        output: Option<WorkflowRunFormat>,
184    },
185    /// Read one durable workflow run snapshot.
186    Status {
187        /// Full run UUID or unique UUID prefix.
188        #[arg(value_name = "RUN_ID")]
189        run_id: String,
190        /// Select human-readable or machine-readable snapshot output.
191        #[arg(long, value_enum, default_value_t)]
192        output: WorkflowDocumentFormat,
193    },
194    /// Request cancellation of a durable workflow run.
195    Cancel {
196        /// Full run UUID or unique UUID prefix.
197        #[arg(value_name = "RUN_ID")]
198        run_id: String,
199    },
200    /// Resume a durable workflow run from its frozen graph.
201    Resume {
202        /// Full run UUID or unique UUID prefix.
203        #[arg(value_name = "RUN_ID")]
204        run_id: String,
205        /// Confirm the frozen graph without an interactive prompt.
206        #[arg(long)]
207        yes: bool,
208        /// Confirm that no prior process remains and relaunch uncertain attempts.
209        #[arg(long)]
210        recover_uncertain: bool,
211        /// Select text or JSON Lines instead of the workflow TUI.
212        #[arg(long, value_enum)]
213        output: Option<WorkflowRunFormat>,
214    },
215}
216
217#[derive(Subcommand, Debug)]
218pub enum SessionsCommand {
219    /// List saved sessions for the current workspace (or all projects).
220    List {
221        /// Include sessions from every workspace, not only the current directory.
222        #[arg(long)]
223        all_projects: bool,
224    },
225    /// Delete one or more sessions by UUID or UUID prefix.
226    Rm {
227        /// Session UUID or unique prefix. May be repeated.
228        #[arg(value_name = "ID", required = true, num_args = 1..)]
229        ids: Vec<String>,
230        /// Delete even when a parent-linked run is still non-terminal.
231        ///
232        /// Use only for stale Starting/Running artifacts left after a crash.
233        #[arg(long)]
234        force: bool,
235        /// Skip the confirmation prompt for cross-project deletes.
236        #[arg(short = 'y', long)]
237        yes: bool,
238    },
239    /// Rename a session by UUID or UUID prefix.
240    Rename {
241        /// Session UUID or unique prefix.
242        #[arg(value_name = "ID")]
243        id_prefix: String,
244        /// New session title. Multiple words are joined with spaces.
245        #[arg(value_name = "TITLE", required = true, num_args = 1.., trailing_var_arg = true)]
246        title: Vec<String>,
247    },
248}
249
250#[derive(Subcommand, Debug)]
251pub enum CredentialStoreCommand {
252    /// Test a credential backend by writing and deleting a temporary secret.
253    Probe {
254        /// Backend to test: os or file (`auto` is accepted as an alias for os).
255        #[arg(
256            value_name = "BACKEND",
257            default_value = "os",
258            value_parser = parse_credential_store_backend
259        )]
260        backend: CredentialStoreBackend,
261    },
262    /// Show the configured credential backend (unset, os, or file).
263    Status,
264    /// Save the credential backend used by future rho processes.
265    Set {
266        /// Backend to use: os or file (`auto` is accepted as an alias for os).
267        #[arg(value_name = "BACKEND", value_parser = parse_credential_store_backend)]
268        backend: CredentialStoreBackend,
269    },
270}
271
272#[cfg(test)]
273#[path = "cli_tests.rs"]
274mod tests;