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 /// Inspect configured Model Context Protocol servers.
149 Mcp {
150 #[command(subcommand)]
151 command: McpCommand,
152 },
153 /// List, inspect, install, and activate local Agent Plugin packages.
154 Plugins {
155 #[command(subcommand)]
156 command: PluginsCommand,
157 },
158 /// Validate, plan, run, and inspect deterministic workflows.
159 Workflow {
160 #[command(subcommand)]
161 command: WorkflowCommand,
162 },
163 /// Internal supervised workflow planner worker. Not a public command.
164 #[command(name = "__workflow_planner_worker", hide = true)]
165 WorkflowPlannerWorker,
166}
167
168/// Argv entry for the internal supervised planner worker process.
169pub const WORKFLOW_PLANNER_WORKER_COMMAND: &str = "__workflow_planner_worker";
170
171#[derive(Subcommand, Debug)]
172pub enum WorkflowCommand {
173 /// List saved workflow plans and runs for the current workspace.
174 List {
175 /// Include only saved plans.
176 #[arg(long, conflicts_with = "runs")]
177 plans: bool,
178 /// Include only runs.
179 #[arg(long, conflicts_with = "plans")]
180 runs: bool,
181 /// Limit how many rows to print per section.
182 #[arg(long, value_name = "N")]
183 limit: Option<NonZeroUsize>,
184 /// Print one JSON document instead of text rows.
185 #[arg(long)]
186 json: bool,
187 },
188 /// Validate a Starlark workflow without creating durable state.
189 Validate {
190 /// Workflow entry file under the current workspace.
191 #[arg(value_name = "FILE")]
192 file: PathBuf,
193 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
194 #[arg(long, value_name = "KEY=JSON")]
195 input: Vec<String>,
196 },
197 /// Validate, freeze, and persist an immutable workflow plan.
198 Plan {
199 /// Workflow entry file under the current workspace.
200 #[arg(value_name = "FILE")]
201 file: PathBuf,
202 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
203 #[arg(long, value_name = "KEY=JSON")]
204 input: Vec<String>,
205 /// Select human-readable or machine-readable plan output.
206 #[arg(long, value_enum, default_value_t)]
207 output: WorkflowDocumentFormat,
208 },
209 /// Create and run a workflow from an immutable plan.
210 Run {
211 /// Full plan UUID or unique UUID prefix.
212 #[arg(value_name = "PLAN_ID")]
213 plan_id: String,
214 /// Confirm the exact plan digest without an interactive prompt.
215 #[arg(long)]
216 yes: bool,
217 /// Select text or JSON Lines instead of the workflow TUI.
218 #[arg(long, value_enum)]
219 output: Option<WorkflowRunFormat>,
220 },
221 /// Read one durable workflow run snapshot.
222 Status {
223 /// Full run UUID or unique UUID prefix.
224 #[arg(value_name = "RUN_ID")]
225 run_id: String,
226 /// Select human-readable or machine-readable snapshot output.
227 #[arg(long, value_enum, default_value_t)]
228 output: WorkflowDocumentFormat,
229 },
230 /// Request cancellation of a durable workflow run.
231 Cancel {
232 /// Full run UUID or unique UUID prefix.
233 #[arg(value_name = "RUN_ID")]
234 run_id: String,
235 },
236 /// Resume a durable workflow run from its frozen graph.
237 Resume {
238 /// Full run UUID or unique UUID prefix.
239 #[arg(value_name = "RUN_ID")]
240 run_id: String,
241 /// Confirm the frozen graph without an interactive prompt.
242 #[arg(long)]
243 yes: bool,
244 /// Confirm that no prior process remains and relaunch uncertain attempts.
245 #[arg(long)]
246 recover_uncertain: bool,
247 /// Select text or JSON Lines instead of the workflow TUI.
248 #[arg(long, value_enum)]
249 output: Option<WorkflowRunFormat>,
250 },
251}
252
253#[derive(Subcommand, Debug)]
254pub enum SessionsCommand {
255 /// List saved sessions for the current workspace (or all projects).
256 List {
257 /// Include sessions from every workspace, not only the current directory.
258 #[arg(long)]
259 all_projects: bool,
260 /// Case-insensitive filter over id, title, and first user message.
261 #[arg(long, short = 'q', value_name = "TEXT")]
262 search: Option<String>,
263 /// Limit how many sessions to print.
264 #[arg(long, value_name = "N")]
265 limit: Option<NonZeroUsize>,
266 /// Print one JSON document instead of text rows.
267 #[arg(long)]
268 json: bool,
269 },
270 /// Export a saved session transcript by UUID or UUID prefix.
271 Export {
272 /// Session UUID or unique prefix.
273 #[arg(value_name = "ID")]
274 id_prefix: String,
275 /// Output path. Omit to write under ~/.rho/exports/.
276 #[arg(long, short = 'o', value_name = "PATH")]
277 output: Option<PathBuf>,
278 /// Explicit format. When omitted, the path extension selects html, md, or json.
279 #[arg(long, value_enum)]
280 format: Option<crate::export::ExportFormat>,
281 /// Overwrite an existing file.
282 #[arg(long)]
283 force: bool,
284 },
285 /// Delete one or more sessions by UUID or UUID prefix.
286 Rm {
287 /// Session UUID or unique prefix. May be repeated.
288 #[arg(value_name = "ID", required = true, num_args = 1..)]
289 ids: Vec<String>,
290 /// Delete even when a parent-linked run is still non-terminal.
291 ///
292 /// Use only for stale Starting/Running artifacts left after a crash.
293 #[arg(long)]
294 force: bool,
295 /// Skip the confirmation prompt for cross-project deletes.
296 #[arg(short = 'y', long)]
297 yes: bool,
298 },
299 /// Rename a session by UUID or UUID prefix.
300 Rename {
301 /// Session UUID or unique prefix.
302 #[arg(value_name = "ID")]
303 id_prefix: String,
304 /// New session title. Multiple words are joined with spaces.
305 #[arg(value_name = "TITLE", required = true, num_args = 1.., trailing_var_arg = true)]
306 title: Vec<String>,
307 },
308}
309
310#[derive(Subcommand, Debug)]
311pub enum McpCommand {
312 /// List configured MCP servers from the selected config and plugins.
313 List {
314 /// Print one JSON document instead of text rows.
315 #[arg(long)]
316 json: bool,
317 /// Start enabled servers and report live connection status.
318 #[arg(long)]
319 connect: bool,
320 },
321 /// Show one MCP server by identity.
322 Show {
323 /// Server table key from `[mcp.servers.<id>]`.
324 #[arg(value_name = "ID")]
325 id: String,
326 /// Print one JSON document instead of text.
327 #[arg(long)]
328 json: bool,
329 /// Start enabled servers and report live connection status.
330 #[arg(long)]
331 connect: bool,
332 },
333}
334
335/// Target scope for plugin install and link.
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
337pub enum PluginsScope {
338 /// User root: `~/.agents/plugins`.
339 #[default]
340 User,
341 /// Project root: `<repository>/.agents/plugins`.
342 Project,
343}
344
345#[derive(Subcommand, Debug)]
346pub enum PluginsCommand {
347 /// List discovered Agent Plugin packages.
348 List {
349 /// Print one JSON document instead of text rows.
350 #[arg(long)]
351 json: bool,
352 },
353 /// Inspect one plugin by package name without executing package code.
354 Inspect {
355 /// Plugin package name from `plugin.json`.
356 #[arg(value_name = "NAME")]
357 name: String,
358 /// Print one JSON document instead of text.
359 #[arg(long)]
360 json: bool,
361 },
362 /// Copy a local plugin package into a managed plugins root.
363 Install {
364 /// Path to a directory that contains `plugin.json`.
365 #[arg(value_name = "PATH")]
366 path: PathBuf,
367 /// Install under the user or project plugins root.
368 #[arg(long, value_enum, default_value_t = PluginsScope::User)]
369 scope: PluginsScope,
370 /// Replace an existing package at the destination.
371 #[arg(long)]
372 force: bool,
373 },
374 /// Symlink a local plugin package into a managed plugins root.
375 Link {
376 /// Path to a directory that contains `plugin.json`.
377 #[arg(value_name = "PATH")]
378 path: PathBuf,
379 /// Link under the user or project plugins root.
380 #[arg(long, value_enum, default_value_t = PluginsScope::User)]
381 scope: PluginsScope,
382 /// Replace an existing package at the destination.
383 #[arg(long)]
384 force: bool,
385 },
386 /// Enable a discovered plugin for new sessions.
387 Enable {
388 /// Plugin package name from `plugin.json`.
389 #[arg(value_name = "NAME")]
390 name: String,
391 },
392 /// Disable a discovered plugin without deleting package files.
393 Disable {
394 /// Plugin package name from `plugin.json`.
395 #[arg(value_name = "NAME")]
396 name: String,
397 },
398 /// Remove an installed or linked package from a managed plugins root.
399 Remove {
400 /// Plugin package name from `plugin.json`.
401 #[arg(value_name = "NAME")]
402 name: String,
403 /// Skip the confirmation prompt.
404 #[arg(short = 'y', long)]
405 yes: bool,
406 },
407}
408
409#[derive(Subcommand, Debug)]
410pub enum CredentialStoreCommand {
411 /// Test a credential backend by writing and deleting a temporary secret.
412 Probe {
413 /// Backend to test: os or file (`auto` is accepted as an alias for os).
414 #[arg(
415 value_name = "BACKEND",
416 default_value = "os",
417 value_parser = parse_credential_store_backend
418 )]
419 backend: CredentialStoreBackend,
420 },
421 /// Show the configured credential backend (unset, os, or file).
422 Status,
423 /// Save the credential backend used by future rho processes.
424 Set {
425 /// Backend to use: os or file (`auto` is accepted as an alias for os).
426 #[arg(value_name = "BACKEND", value_parser = parse_credential_store_backend)]
427 backend: CredentialStoreBackend,
428 },
429}
430
431#[cfg(test)]
432#[path = "cli_tests.rs"]
433mod tests;