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}