1use std::collections::BTreeMap;
4
5use serde_json::{json, Value};
6
7use crate::coerce::coerce_value;
8use crate::error::{Error, Result};
9use crate::model::{CommandDef, ParamType};
10
11#[derive(Debug, Clone)]
12pub struct ParsedToolArgs {
13 pub command: CommandDef,
14 pub values: BTreeMap<String, Value>,
15 pub stdin: bool,
16}
17
18pub fn parse_tool_args(commands: &[CommandDef], remaining: &[String]) -> Result<ParsedToolArgs> {
20 if remaining.is_empty() {
21 return Err(Error::usage(
22 "no subcommand specified. Use --list to see available tools.",
23 ));
24 }
25 if remaining[0] == "-h" || remaining[0] == "--help" {
26 return Err(Error::usage("use --list to see available commands"));
27 }
28
29 let name = &remaining[0];
30 let cmd = commands
31 .iter()
32 .find(|c| &c.name == name)
33 .cloned()
34 .ok_or_else(|| Error::usage(format!("unknown command: {name}")))?;
35
36 if remaining.get(1).map(String::as_str) == Some("-h")
37 || remaining.get(1).map(String::as_str) == Some("--help")
38 {
39 print_command_help(&cmd);
40 return Err(Error::usage("__help__"));
41 }
42
43 let mut values = BTreeMap::new();
44 let mut stdin = false;
45 let mut i = 1;
46 while i < remaining.len() {
47 let arg = &remaining[i];
48 if arg == "--stdin" {
49 stdin = true;
50 i += 1;
51 continue;
52 }
53 if !arg.starts_with("--") {
54 return Err(Error::usage(format!("unexpected argument: {arg}")));
55 }
56 let flag = arg.trim_start_matches("--");
57 let param = cmd
58 .params
59 .iter()
60 .find(|p| p.name == flag)
61 .ok_or_else(|| Error::usage(format!("unknown option: {arg}")))?;
62
63 match param.python_type {
64 ParamType::Boolean if param.location == crate::model::ParamLocation::GraphqlArg => {
65 i += 1;
67 if let Some(raw) = remaining.get(i) {
68 let lower = raw.to_lowercase();
69 if matches!(lower.as_str(), "true" | "false" | "1" | "0" | "yes" | "no") {
70 let coerced = coerce_value(Some(Value::String(raw.clone())), ¶m.schema)
71 .unwrap_or(Value::Bool(matches!(lower.as_str(), "true" | "1" | "yes")));
72 values.insert(param.original_name.clone(), coerced);
73 i += 1;
74 continue;
75 }
76 }
77 values.insert(param.original_name.clone(), Value::Bool(true));
78 }
79 ParamType::Boolean => {
80 values.insert(param.original_name.clone(), Value::Bool(true));
81 i += 1;
82 }
83 other => {
84 i += 1;
85 let Some(raw) = remaining.get(i) else {
86 return Err(Error::usage(format!("option {arg} requires a value")));
87 };
88 if let Some(choices) = ¶m.choices {
89 if !choices.iter().any(|c| c == raw) {
90 return Err(Error::usage(format!(
91 "invalid value {raw:?} for {arg}; choices: {choices:?}"
92 )));
93 }
94 }
95 let as_value = match other {
96 ParamType::Integer => json!(raw.parse::<i64>().map_err(|_| {
97 Error::usage(format!("invalid integer for {arg}: {raw}"))
98 })?),
99 ParamType::Float => json!(raw.parse::<f64>().map_err(|_| {
100 Error::usage(format!("invalid number for {arg}: {raw}"))
101 })?),
102 ParamType::String | ParamType::Boolean => Value::String(raw.clone()),
103 };
104 let coerced = coerce_value(Some(as_value), ¶m.schema)
105 .unwrap_or(Value::String(raw.clone()));
106 values.insert(param.original_name.clone(), coerced);
107 i += 1;
108 }
109 }
110 }
111
112 for p in &cmd.params {
114 if !p.required {
115 continue;
116 }
117 if matches!(
118 p.location,
119 crate::model::ParamLocation::Body
120 | crate::model::ParamLocation::ToolInput
121 | crate::model::ParamLocation::GraphqlArg
122 ) {
123 continue;
124 }
125 if p.python_type == ParamType::Boolean {
126 continue;
127 }
128 if !values.contains_key(&p.original_name) && !stdin {
129 return Err(Error::usage(format!(
130 "missing required argument: --{}",
131 p.name
132 )));
133 }
134 }
135
136 Ok(ParsedToolArgs {
137 command: cmd,
138 values,
139 stdin,
140 })
141}
142
143pub fn print_command_help(cmd: &CommandDef) {
144 println!("{}", cmd.name);
145 if !cmd.description.is_empty() {
146 println!(" {}", cmd.description);
147 }
148 println!();
149 if cmd.has_body {
150 println!(" --stdin Read JSON body/arguments from stdin");
151 }
152 for p in &cmd.params {
153 let req = if p.required { " (required)" } else { "" };
154 let ty = p.python_type.type_name();
155 println!(" --{:<20} [{}]{} {}", p.name, ty, req, p.description);
156 }
157}
158
159pub fn read_stdin_json(context: &str) -> Result<Value> {
160 use std::io::Read;
161 let mut raw = String::new();
162 std::io::stdin()
163 .read_to_string(&mut raw)
164 .map_err(Error::from)?;
165 if raw.trim().is_empty() {
166 return Err(Error::runtime(format!(
167 "--stdin expects JSON for {context}, but stdin was empty."
168 )));
169 }
170 serde_json::from_str(&raw).map_err(|exc| {
171 Error::runtime(format!(
172 "invalid JSON on stdin for {context} (line {}, column {}).",
173 exc.line(),
174 exc.column()
175 ))
176 })
177}