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