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::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;