Skip to main content

vv_agent/cli/
args.rs

1use std::env;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct CliArgs {
6    pub prompt: String,
7    pub backend: String,
8    pub model: String,
9    pub settings_file: PathBuf,
10    pub workspace: PathBuf,
11    pub max_cycles: u32,
12    pub language: String,
13    pub agent_type: Option<String>,
14    pub verbose: bool,
15}
16
17pub fn parse_cli_args_from<I, S>(args: I) -> Result<CliArgs, String>
18where
19    I: IntoIterator<Item = S>,
20    S: Into<String>,
21{
22    let default_settings =
23        env::var("VV_AGENT_LOCAL_SETTINGS").unwrap_or_else(|_| "local_settings.json".to_string());
24    parse_cli_args_from_with_default_settings(args, default_settings)
25}
26
27pub fn parse_cli_args_from_with_default_settings<I, S>(
28    args: I,
29    default_settings: impl Into<String>,
30) -> Result<CliArgs, String>
31where
32    I: IntoIterator<Item = S>,
33    S: Into<String>,
34{
35    let mut values = args.into_iter().map(Into::into).collect::<Vec<_>>();
36    if !values.is_empty() {
37        values.remove(0);
38    }
39
40    let mut parsed = ParsedCliArgs::with_default_settings(default_settings.into());
41    parsed.consume(values)?;
42    parsed.finish()
43}
44
45struct ParsedCliArgs {
46    prompt: Option<String>,
47    backend: String,
48    model: String,
49    settings_file: PathBuf,
50    workspace: PathBuf,
51    max_cycles: u32,
52    language: String,
53    agent_type: Option<String>,
54    verbose: bool,
55}
56
57impl ParsedCliArgs {
58    fn with_default_settings(default_settings: String) -> Self {
59        Self {
60            prompt: None,
61            backend: "moonshot".to_string(),
62            model: "kimi-k2.6".to_string(),
63            settings_file: PathBuf::from(default_settings),
64            workspace: PathBuf::from("./workspace"),
65            max_cycles: 80,
66            language: "zh-CN".to_string(),
67            agent_type: None,
68            verbose: false,
69        }
70    }
71
72    fn consume(&mut self, values: Vec<String>) -> Result<(), String> {
73        let mut index = 0;
74        while index < values.len() {
75            let flag = &values[index];
76            index += 1;
77            match flag.as_str() {
78                "--prompt" => self.prompt = Some(next_value(&values, &mut index, "--prompt")?),
79                "--backend" => self.backend = next_value(&values, &mut index, "--backend")?,
80                "--model" => self.model = next_value(&values, &mut index, "--model")?,
81                "--settings-file" => {
82                    self.settings_file =
83                        PathBuf::from(next_value(&values, &mut index, "--settings-file")?)
84                }
85                "--workspace" => {
86                    self.workspace = PathBuf::from(next_value(&values, &mut index, "--workspace")?)
87                }
88                "--max-cycles" => {
89                    let raw = next_value(&values, &mut index, "--max-cycles")?;
90                    self.max_cycles = raw
91                        .parse::<u32>()
92                        .map_err(|_| "--max-cycles must be an integer".to_string())?
93                        .max(1);
94                }
95                "--language" => self.language = next_value(&values, &mut index, "--language")?,
96                "--agent-type" => {
97                    self.agent_type = Some(next_value(&values, &mut index, "--agent-type")?)
98                        .filter(|value| !value.trim().is_empty())
99                }
100                "--verbose" => self.verbose = true,
101                "--help" | "-h" => return Err(help_text()),
102                other => return Err(format!("unknown argument: {other}\n\n{}", help_text())),
103            }
104        }
105        Ok(())
106    }
107
108    fn finish(self) -> Result<CliArgs, String> {
109        let Some(prompt) = self.prompt.filter(|value| !value.trim().is_empty()) else {
110            return Err(format!("--prompt is required\n\n{}", help_text()));
111        };
112
113        Ok(CliArgs {
114            prompt,
115            backend: self.backend,
116            model: self.model,
117            settings_file: self.settings_file,
118            workspace: self.workspace,
119            max_cycles: self.max_cycles,
120            language: self.language,
121            agent_type: self.agent_type,
122            verbose: self.verbose,
123        })
124    }
125}
126
127fn next_value(values: &[String], index: &mut usize, flag: &str) -> Result<String, String> {
128    let Some(value) = values.get(*index) else {
129        return Err(format!("{flag} requires a value"));
130    };
131    *index += 1;
132    Ok(value.clone())
133}
134
135pub(crate) fn help_text() -> String {
136    [
137        "Run a vv-agent task against a configured LLM endpoint.",
138        "",
139        "Required:",
140        "  --prompt <text>",
141        "",
142        "Options:",
143        "  --backend <key>        Provider backend key in LLM_SETTINGS (default: moonshot)",
144        "  --model <key>          Model key in provider models (default: kimi-k2.6)",
145        "  --settings-file <path> Path to local settings (default: VV_AGENT_LOCAL_SETTINGS or local_settings.json)",
146        "  --workspace <path>     Workspace directory (default: ./workspace)",
147        "  --max-cycles <n>       Max runtime cycles (default: 80)",
148        "  --language <locale>    System prompt language (default: zh-CN)",
149        "  --agent-type <type>    Agent type, e.g. computer",
150        "  --verbose             Show per-cycle runtime logs",
151    ]
152    .join("\n")
153}