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 /// Persist --provider/--model/--auth/--reasoning overrides to the config file.
80 ///
81 /// Without this flag, those overrides apply only to the current invocation.
82 #[arg(long)]
83 pub save: bool,
84 /// Resume an existing session by UUID or UUID prefix. Omit the ID to choose from a picker.
85 #[arg(short = 'R', long, value_name = "ID", num_args = 0..=1)]
86 pub resume: Option<Option<String>>,
87 #[command(subcommand)]
88 pub command: Option<Command>,
89}
90
91#[derive(Subcommand, Debug)]
92pub enum Command {
93 /// Run one non-interactive automation prompt and print the final answer.
94 Run {
95 /// Read additional prompt text from stdin.
96 ///
97 /// Required when stdin is a pipe or redirected file. Without this flag,
98 /// redirected stdin is rejected so prompt text is not silently dropped.
99 #[arg(long)]
100 stdin: bool,
101 /// Write a structured status/result file (JSON) that is updated during
102 /// the run and finalized on exit. With `--output text` (the default),
103 /// progress and streamed assistant text go to stdout and the run ends
104 /// with a completion marker; the result file is the durable final
105 /// answer. With `--output jsonl`, stdout stays the JSONL event stream.
106 #[arg(long, value_name = "PATH")]
107 output_file: Option<PathBuf>,
108 /// Select plain final-answer output or a JSON Lines event stream.
109 #[arg(long, value_enum, default_value_t)]
110 output: OutputFormat,
111 /// Override the model-step budget for this run.
112 #[arg(long, value_name = "N")]
113 max_steps: Option<NonZeroUsize>,
114 /// Cancel the run after this wall-clock duration.
115 #[arg(long, value_name = "DURATION", value_parser = parse_duration)]
116 timeout: Option<Duration>,
117 /// Prompt text to send to the agent.
118 #[arg(value_name = "PROMPT", num_args = 0..)]
119 prompt: Vec<String>,
120 },
121 /// Watch a delegated agent run in a read-only TUI.
122 Attach {
123 /// Delegated run ID shown when the agent was started.
124 #[arg(value_name = "ID")]
125 id: String,
126 },
127 /// Log in to a provider from a browser or device-code flow.
128 Login {
129 /// Provider to authenticate, for example openai-codex or github-copilot.
130 #[arg(value_name = "PROVIDER")]
131 provider: String,
132 /// Use device-code login instead of opening a local browser callback.
133 #[arg(long)]
134 device_auth: bool,
135 },
136 /// Configure or probe provider credential storage.
137 CredentialStore {
138 #[command(subcommand)]
139 command: CredentialStoreCommand,
140 },
141 /// Update rho using the detected installation method.
142 Update,
143 /// List, rename, or delete saved sessions.
144 Sessions {
145 #[command(subcommand)]
146 command: SessionsCommand,
147 },
148 /// Validate, plan, run, and inspect deterministic workflows.
149 Workflow {
150 #[command(subcommand)]
151 command: WorkflowCommand,
152 },
153 /// Internal supervised workflow planner worker. Not a public command.
154 #[command(name = "__workflow_planner_worker", hide = true)]
155 WorkflowPlannerWorker,
156}
157
158/// Argv entry for the internal supervised planner worker process.
159pub const WORKFLOW_PLANNER_WORKER_COMMAND: &str = "__workflow_planner_worker";
160
161#[derive(Subcommand, Debug)]
162pub enum WorkflowCommand {
163 /// List saved workflow plans and runs for the current workspace.
164 List {
165 /// Include only saved plans.
166 #[arg(long, conflicts_with = "runs")]
167 plans: bool,
168 /// Include only runs.
169 #[arg(long, conflicts_with = "plans")]
170 runs: bool,
171 /// Limit how many rows to print per section.
172 #[arg(long, value_name = "N")]
173 limit: Option<NonZeroUsize>,
174 /// Print one JSON document instead of text rows.
175 #[arg(long)]
176 json: bool,
177 },
178 /// Validate a Starlark workflow without creating durable state.
179 Validate {
180 /// Workflow entry file under the current workspace.
181 #[arg(value_name = "FILE")]
182 file: PathBuf,
183 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
184 #[arg(long, value_name = "KEY=JSON")]
185 input: Vec<String>,
186 },
187 /// Validate, freeze, and persist an immutable workflow plan.
188 Plan {
189 /// Workflow entry file under the current workspace.
190 #[arg(value_name = "FILE")]
191 file: PathBuf,
192 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
193 #[arg(long, value_name = "KEY=JSON")]
194 input: Vec<String>,
195 /// Select human-readable or machine-readable plan output.
196 #[arg(long, value_enum, default_value_t)]
197 output: WorkflowDocumentFormat,
198 },
199 /// Create and run a workflow from an immutable plan.
200 Run {
201 /// Full plan UUID or unique UUID prefix.
202 #[arg(value_name = "PLAN_ID")]
203 plan_id: String,
204 /// Confirm the exact plan digest without an interactive prompt.
205 #[arg(long)]
206 yes: bool,
207 /// Select text or JSON Lines instead of the workflow TUI.
208 #[arg(long, value_enum)]
209 output: Option<WorkflowRunFormat>,
210 },
211 /// Read one durable workflow run snapshot.
212 Status {
213 /// Full run UUID or unique UUID prefix.
214 #[arg(value_name = "RUN_ID")]
215 run_id: String,
216 /// Select human-readable or machine-readable snapshot output.
217 #[arg(long, value_enum, default_value_t)]
218 output: WorkflowDocumentFormat,
219 },
220 /// Request cancellation of a durable workflow run.
221 Cancel {
222 /// Full run UUID or unique UUID prefix.
223 #[arg(value_name = "RUN_ID")]
224 run_id: String,
225 },
226 /// Resume a durable workflow run from its frozen graph.
227 Resume {
228 /// Full run UUID or unique UUID prefix.
229 #[arg(value_name = "RUN_ID")]
230 run_id: String,
231 /// Confirm the frozen graph without an interactive prompt.
232 #[arg(long)]
233 yes: bool,
234 /// Confirm that no prior process remains and relaunch uncertain attempts.
235 #[arg(long)]
236 recover_uncertain: bool,
237 /// Select text or JSON Lines instead of the workflow TUI.
238 #[arg(long, value_enum)]
239 output: Option<WorkflowRunFormat>,
240 },
241}
242
243#[derive(Subcommand, Debug)]
244pub enum SessionsCommand {
245 /// List saved sessions for the current workspace (or all projects).
246 List {
247 /// Include sessions from every workspace, not only the current directory.
248 #[arg(long)]
249 all_projects: bool,
250 /// Case-insensitive filter over id, title, and first user message.
251 #[arg(long, short = 'q', value_name = "TEXT")]
252 search: Option<String>,
253 /// Limit how many sessions to print.
254 #[arg(long, value_name = "N")]
255 limit: Option<NonZeroUsize>,
256 /// Print one JSON document instead of text rows.
257 #[arg(long)]
258 json: bool,
259 },
260 /// Export a saved session transcript by UUID or UUID prefix.
261 Export {
262 /// Session UUID or unique prefix.
263 #[arg(value_name = "ID")]
264 id_prefix: String,
265 /// Output path. Omit to write under ~/.rho/exports/.
266 #[arg(long, short = 'o', value_name = "PATH")]
267 output: Option<PathBuf>,
268 /// Explicit format. When omitted, the path extension selects html, md, or json.
269 #[arg(long, value_enum)]
270 format: Option<crate::export::ExportFormat>,
271 /// Overwrite an existing file.
272 #[arg(long)]
273 force: bool,
274 },
275 /// Delete one or more sessions by UUID or UUID prefix.
276 Rm {
277 /// Session UUID or unique prefix. May be repeated.
278 #[arg(value_name = "ID", required = true, num_args = 1..)]
279 ids: Vec<String>,
280 /// Delete even when a parent-linked run is still non-terminal.
281 ///
282 /// Use only for stale Starting/Running artifacts left after a crash.
283 #[arg(long)]
284 force: bool,
285 /// Skip the confirmation prompt for cross-project deletes.
286 #[arg(short = 'y', long)]
287 yes: bool,
288 },
289 /// Rename a session by UUID or UUID prefix.
290 Rename {
291 /// Session UUID or unique prefix.
292 #[arg(value_name = "ID")]
293 id_prefix: String,
294 /// New session title. Multiple words are joined with spaces.
295 #[arg(value_name = "TITLE", required = true, num_args = 1.., trailing_var_arg = true)]
296 title: Vec<String>,
297 },
298}
299
300#[derive(Subcommand, Debug)]
301pub enum CredentialStoreCommand {
302 /// Test a credential backend by writing and deleting a temporary secret.
303 Probe {
304 /// Backend to test: os or file (`auto` is accepted as an alias for os).
305 #[arg(
306 value_name = "BACKEND",
307 default_value = "os",
308 value_parser = parse_credential_store_backend
309 )]
310 backend: CredentialStoreBackend,
311 },
312 /// Show the configured credential backend (unset, os, or file).
313 Status,
314 /// Save the credential backend used by future rho processes.
315 Set {
316 /// Backend to use: os or file (`auto` is accepted as an alias for os).
317 #[arg(value_name = "BACKEND", value_parser = parse_credential_store_backend)]
318 backend: CredentialStoreBackend,
319 },
320}
321
322#[cfg(test)]
323#[path = "cli_tests.rs"]
324mod tests;