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;