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
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum CliCommand {
19 Run(CliArgs),
20 AppServer(AppServerCliCommand),
21 Debug(DebugCliCommand),
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum AppServerCliCommand {
26 ListenStdio,
27 GenerateTs { out: PathBuf },
28 GenerateJsonSchema { out: PathBuf },
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum DebugCliCommand {
33 AppServerSendMessage { message: String },
34}
35
36pub fn parse_cli_args_from<I, S>(args: I) -> Result<CliArgs, String>
37where
38 I: IntoIterator<Item = S>,
39 S: Into<String>,
40{
41 let default_settings =
42 env::var("VV_AGENT_LOCAL_SETTINGS").unwrap_or_else(|_| "local_settings.json".to_string());
43 parse_cli_args_from_with_default_settings(args, default_settings)
44}
45
46pub fn parse_cli_command_from<I, S>(args: I) -> Result<CliCommand, String>
47where
48 I: IntoIterator<Item = S>,
49 S: Into<String>,
50{
51 let default_settings =
52 env::var("VV_AGENT_LOCAL_SETTINGS").unwrap_or_else(|_| "local_settings.json".to_string());
53 parse_cli_command_from_with_default_settings(args, default_settings)
54}
55
56pub fn parse_cli_command_from_with_default_settings<I, S>(
57 args: I,
58 default_settings: impl Into<String>,
59) -> Result<CliCommand, String>
60where
61 I: IntoIterator<Item = S>,
62 S: Into<String>,
63{
64 let mut values = args.into_iter().map(Into::into).collect::<Vec<_>>();
65 if !values.is_empty() {
66 values.remove(0);
67 }
68 match values.first().map(String::as_str) {
69 Some("app-server") => parse_app_server_command(&values[1..]).map(CliCommand::AppServer),
70 Some("debug") => parse_debug_command(&values[1..]).map(CliCommand::Debug),
71 _ => parse_cli_args_from_with_default_settings(
72 std::iter::once("vv-agent".to_string()).chain(values),
73 default_settings,
74 )
75 .map(CliCommand::Run),
76 }
77}
78
79pub fn parse_cli_args_from_with_default_settings<I, S>(
80 args: I,
81 default_settings: impl Into<String>,
82) -> Result<CliArgs, String>
83where
84 I: IntoIterator<Item = S>,
85 S: Into<String>,
86{
87 let mut values = args.into_iter().map(Into::into).collect::<Vec<_>>();
88 if !values.is_empty() {
89 values.remove(0);
90 }
91
92 let mut parsed = ParsedCliArgs::with_default_settings(default_settings.into());
93 parsed.consume(values)?;
94 parsed.finish()
95}
96
97fn parse_app_server_command(values: &[String]) -> Result<AppServerCliCommand, String> {
98 match values.first().map(String::as_str) {
99 Some("--listen") => {
100 let listen = values
101 .get(1)
102 .ok_or_else(|| "app-server --listen requires a value".to_string())?;
103 if listen != "stdio" {
104 return Err("only app-server --listen stdio is supported".to_string());
105 }
106 Ok(AppServerCliCommand::ListenStdio)
107 }
108 Some("generate-ts") => Ok(AppServerCliCommand::GenerateTs {
109 out: parse_out_dir(&values[1..], "app-server generate-ts")?,
110 }),
111 Some("generate-json-schema") => Ok(AppServerCliCommand::GenerateJsonSchema {
112 out: parse_out_dir(&values[1..], "app-server generate-json-schema")?,
113 }),
114 _ => Err(format!("unknown app-server command\n\n{}", help_text())),
115 }
116}
117
118fn parse_debug_command(values: &[String]) -> Result<DebugCliCommand, String> {
119 match (
120 values.first().map(String::as_str),
121 values.get(1).map(String::as_str),
122 values.get(2).map(String::as_str),
123 ) {
124 (Some("app-server"), Some("send-message"), Some(message)) => {
125 Ok(DebugCliCommand::AppServerSendMessage {
126 message: message.to_string(),
127 })
128 }
129 _ => Err(format!("unknown debug command\n\n{}", help_text())),
130 }
131}
132
133fn parse_out_dir(values: &[String], command: &str) -> Result<PathBuf, String> {
134 if values.first().map(String::as_str) != Some("--out") {
135 return Err(format!("{command} requires --out <dir>"));
136 }
137 values
138 .get(1)
139 .map(PathBuf::from)
140 .ok_or_else(|| format!("{command} requires --out <dir>"))
141}
142
143struct ParsedCliArgs {
144 prompt: Option<String>,
145 backend: String,
146 model: String,
147 settings_file: PathBuf,
148 workspace: PathBuf,
149 max_cycles: u32,
150 language: String,
151 agent_type: Option<String>,
152 verbose: bool,
153}
154
155impl ParsedCliArgs {
156 fn with_default_settings(default_settings: String) -> Self {
157 Self {
158 prompt: None,
159 backend: "moonshot".to_string(),
160 model: "kimi-k2.6".to_string(),
161 settings_file: PathBuf::from(default_settings),
162 workspace: PathBuf::from("./workspace"),
163 max_cycles: 80,
164 language: "zh-CN".to_string(),
165 agent_type: None,
166 verbose: false,
167 }
168 }
169
170 fn consume(&mut self, values: Vec<String>) -> Result<(), String> {
171 let mut index = 0;
172 while index < values.len() {
173 let flag = &values[index];
174 index += 1;
175 match flag.as_str() {
176 "--prompt" => self.prompt = Some(next_value(&values, &mut index, "--prompt")?),
177 "--backend" => self.backend = next_value(&values, &mut index, "--backend")?,
178 "--model" => self.model = next_value(&values, &mut index, "--model")?,
179 "--settings-file" => {
180 self.settings_file =
181 PathBuf::from(next_value(&values, &mut index, "--settings-file")?)
182 }
183 "--workspace" => {
184 self.workspace = PathBuf::from(next_value(&values, &mut index, "--workspace")?)
185 }
186 "--max-cycles" => {
187 let raw = next_value(&values, &mut index, "--max-cycles")?;
188 self.max_cycles = raw
189 .parse::<u32>()
190 .map_err(|_| "--max-cycles must be an integer".to_string())?
191 .max(1);
192 }
193 "--language" => self.language = next_value(&values, &mut index, "--language")?,
194 "--agent-type" => {
195 self.agent_type = Some(next_value(&values, &mut index, "--agent-type")?)
196 .filter(|value| !value.trim().is_empty())
197 }
198 "--verbose" => self.verbose = true,
199 "--help" | "-h" => return Err(help_text()),
200 other => return Err(format!("unknown argument: {other}\n\n{}", help_text())),
201 }
202 }
203 Ok(())
204 }
205
206 fn finish(self) -> Result<CliArgs, String> {
207 let Some(prompt) = self.prompt.filter(|value| !value.trim().is_empty()) else {
208 return Err(format!("--prompt is required\n\n{}", help_text()));
209 };
210
211 Ok(CliArgs {
212 prompt,
213 backend: self.backend,
214 model: self.model,
215 settings_file: self.settings_file,
216 workspace: self.workspace,
217 max_cycles: self.max_cycles,
218 language: self.language,
219 agent_type: self.agent_type,
220 verbose: self.verbose,
221 })
222 }
223}
224
225fn next_value(values: &[String], index: &mut usize, flag: &str) -> Result<String, String> {
226 let Some(value) = values.get(*index) else {
227 return Err(format!("{flag} requires a value"));
228 };
229 *index += 1;
230 Ok(value.clone())
231}
232
233pub(crate) fn help_text() -> String {
234 [
235 "Run a vv-agent task against a configured LLM endpoint.",
236 "",
237 "Required:",
238 " --prompt <text>",
239 "",
240 "Options:",
241 " --backend <key> Provider backend key in LLM_SETTINGS (default: moonshot)",
242 " --model <key> Model key in provider models (default: kimi-k2.6)",
243 " --settings-file <path> Path to local settings (default: VV_AGENT_LOCAL_SETTINGS or local_settings.json)",
244 " --workspace <path> Workspace directory (default: ./workspace)",
245 " --max-cycles <n> Max runtime cycles (default: 80)",
246 " --language <locale> System prompt language (default: zh-CN)",
247 " --agent-type <type> Agent type, e.g. computer",
248 " --verbose Show per-cycle runtime logs",
249 ]
250 .join("\n")
251}