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        #[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;