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