Skip to main content

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;