1use std::collections::HashMap;
4
5use crate::models::{ToolInvocation, ToolStatistics};
6
7#[allow(dead_code)] const EXCLUDED_SHELL_BUILTINS: &[&str] = &[
10 "cd", "ls", "pwd", "echo", "cat", "mkdir", "rm", "cp", "mv", "export", "source", "if", "then",
11 "else", "fi", "for", "while", "do", "done", "case", "esac", "function", "return", "local",
12 "set", "unset", "shift", "test", "[", "[[", "alias", "unalias", "bg", "fg", "jobs", "wait",
13 "kill", "exit", "break", "continue", "read", "printf", "pushd", "popd", "dirs", "true",
14 "false", ":", ".",
15];
16
17pub fn parse_command_context(
26 command: &str,
27 tool_start: usize,
28) -> Option<(String, Vec<String>, HashMap<String, String>)> {
29 let cmd_parts = split_command_pipeline(command);
31
32 let relevant_part = cmd_parts
34 .iter()
35 .find(|part| {
36 if let Some(offset) = command.find(*part) {
38 tool_start >= offset && tool_start < offset + part.len()
39 } else {
40 false
41 }
42 })?
43 .trim();
44
45 let tokens: Vec<String> = shell_words::split(relevant_part).ok()?;
47
48 if tokens.is_empty() {
49 return None;
50 }
51
52 let mut args = Vec::new();
53 let mut flags = HashMap::new();
54
55 let mut i = 1; while i < tokens.len() {
57 let token = &tokens[i];
58
59 if token.starts_with("--") {
60 let flag_name = token.trim_start_matches("--");
62 let flag_value = tokens.get(i + 1).cloned().unwrap_or_default();
63 flags.insert(flag_name.to_string(), flag_value);
64 i += 2;
65 } else if token.starts_with('-') && token.len() > 1 {
66 let flag_name = token.trim_start_matches('-');
68 let flag_value = tokens.get(i + 1).cloned().unwrap_or_default();
69 flags.insert(flag_name.to_string(), flag_value);
70 i += 2;
71 } else {
72 args.push(token.clone());
74 i += 1;
75 }
76 }
77
78 Some((relevant_part.to_string(), args, flags))
79}
80
81pub fn split_command_pipeline(command: &str) -> Vec<String> {
83 let mut parts = Vec::new();
84 let mut current = String::new();
85 let mut in_quotes = false;
86 let mut quote_char = ' ';
87
88 let chars: Vec<char> = command.chars().collect();
89 let mut i = 0;
90
91 while i < chars.len() {
92 let ch = chars[i];
93
94 match ch {
95 '"' | '\'' if !in_quotes => {
96 in_quotes = true;
97 quote_char = ch;
98 current.push(ch);
99 }
100 '"' | '\'' if in_quotes && ch == quote_char => {
101 in_quotes = false;
102 current.push(ch);
103 }
104 '&' | '|' | ';' if !in_quotes => {
105 if (ch == '&' || ch == '|') && i + 1 < chars.len() && chars[i + 1] == ch {
107 if !current.trim().is_empty() {
108 parts.push(current.trim().to_string());
109 current.clear();
110 }
111 i += 2;
112 continue;
113 }
114 if !current.trim().is_empty() {
115 parts.push(current.trim().to_string());
116 current.clear();
117 }
118 }
119 _ => current.push(ch),
120 }
121 i += 1;
122 }
123
124 if !current.trim().is_empty() {
125 parts.push(current.trim().to_string());
126 }
127
128 parts
129}
130
131#[must_use]
133#[allow(dead_code)] pub fn is_actual_tool(tool_name: &str) -> bool {
135 let base_name = tool_name.rsplit('/').next().unwrap_or(tool_name).trim();
137
138 !EXCLUDED_SHELL_BUILTINS.contains(&base_name)
140}
141
142#[must_use]
145#[allow(dead_code)]
146pub fn calculate_tool_statistics(
147 invocations: &[ToolInvocation],
148) -> HashMap<String, ToolStatistics> {
149 let mut stats: HashMap<String, ToolStatistics> = HashMap::new();
150
151 for inv in invocations {
152 let stat = stats
153 .entry(inv.tool_name.clone())
154 .or_insert_with(|| ToolStatistics {
155 tool_name: inv.tool_name.clone(),
156 category: inv.tool_category.clone(),
157 total_invocations: 0,
158 agents_using: Vec::new(),
159 success_count: 0,
160 failure_count: 0,
161 first_seen: inv.timestamp,
162 last_seen: inv.timestamp,
163 command_patterns: Vec::new(),
164 sessions: Vec::new(),
165 });
166
167 stat.total_invocations += 1;
168
169 if let Some(ref agent) = inv.agent_context {
171 if !stat.agents_using.contains(agent) {
172 stat.agents_using.push(agent.clone());
173 }
174 }
175
176 if !stat.sessions.contains(&inv.session_id) {
178 stat.sessions.push(inv.session_id.clone());
179 }
180
181 if inv.timestamp < stat.first_seen {
183 stat.first_seen = inv.timestamp;
184 }
185 if inv.timestamp > stat.last_seen {
186 stat.last_seen = inv.timestamp;
187 }
188
189 match inv.exit_code {
191 Some(0) => stat.success_count += 1,
192 Some(_) => stat.failure_count += 1,
193 None => {}
194 }
195
196 let base_cmd = format!("{} {}", inv.tool_name, inv.arguments.join(" "));
198 if !stat.command_patterns.contains(&base_cmd) && stat.command_patterns.len() < 10 {
199 stat.command_patterns.push(base_cmd);
200 }
201 }
202
203 stats
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_parse_command_context() {
212 let cmd = "npx wrangler deploy --env production";
213 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
214
215 assert!(full.contains("wrangler"));
216 assert!(args.contains(&"deploy".to_string()));
217 assert_eq!(flags.get("env"), Some(&"production".to_string()));
218 }
219
220 #[test]
221 fn test_split_command_pipeline() {
222 let cmd = "npm install && npm build";
223 let parts = split_command_pipeline(cmd);
224
225 assert_eq!(parts.len(), 2);
226 assert_eq!(parts[0], "npm install");
227 assert_eq!(parts[1], "npm build");
228 }
229
230 #[test]
231 fn test_split_with_quotes() {
232 let cmd = r#"echo "hello && world" && npm install"#;
233 let parts = split_command_pipeline(cmd);
234
235 assert_eq!(parts.len(), 2);
236 assert!(parts[0].contains("hello && world"));
237 }
238
239 #[test]
240 fn test_split_with_pipe() {
241 let cmd = "cat file.txt | grep pattern";
242 let parts = split_command_pipeline(cmd);
243
244 assert_eq!(parts.len(), 2);
245 assert_eq!(parts[0], "cat file.txt");
246 assert_eq!(parts[1], "grep pattern");
247 }
248
249 #[test]
251 fn test_parse_wrangler_deploy_with_env() {
252 let cmd = "npx wrangler deploy --env production";
253 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
254
255 assert!(full.contains("wrangler"));
256 assert_eq!(args, vec!["wrangler", "deploy"]);
257 assert_eq!(flags.get("env"), Some(&"production".to_string()));
258 }
259
260 #[test]
261 fn test_parse_wrangler_complex_flags() {
262 let cmd = "npx wrangler deploy --env prod --minify --compatibility-date 2024-01-01";
263 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
264
265 assert!(full.contains("wrangler"));
266 assert_eq!(args, vec!["wrangler", "deploy", "2024-01-01"]);
267 assert_eq!(flags.get("env"), Some(&"prod".to_string()));
268 assert_eq!(
269 flags.get("minify"),
270 Some(&"--compatibility-date".to_string())
271 );
272 }
273
274 #[test]
275 fn test_parse_wrangler_bunx() {
276 let cmd = "bunx wrangler login";
277 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
278
279 assert!(full.contains("wrangler"));
280 assert_eq!(args, vec!["wrangler", "login"]);
281 assert!(flags.is_empty());
282 }
283
284 #[test]
285 fn test_parse_wrangler_pnpm() {
286 let cmd = "pnpm wrangler deploy --env staging";
287 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
288
289 assert!(full.contains("wrangler"));
290 assert_eq!(args, vec!["wrangler", "deploy"]);
291 assert_eq!(flags.get("env"), Some(&"staging".to_string()));
292 }
293
294 #[test]
295 fn test_parse_wrangler_yarn() {
296 let cmd = "yarn wrangler publish";
297 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
298
299 assert!(full.contains("wrangler"));
300 assert_eq!(args, vec!["wrangler", "publish"]);
301 assert!(flags.is_empty());
302 }
303
304 #[test]
305 fn test_parse_wrangler_dev() {
306 let cmd = "npx wrangler dev --port 8787";
307 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
308
309 assert!(full.contains("wrangler"));
310 assert_eq!(args, vec!["wrangler", "dev"]);
311 assert_eq!(flags.get("port"), Some(&"8787".to_string()));
312 }
313
314 #[test]
315 fn test_parse_wrangler_tail() {
316 let cmd = "bunx wrangler tail my-worker";
317 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
318
319 assert!(full.contains("wrangler"));
320 assert_eq!(args, vec!["wrangler", "tail", "my-worker"]);
321 assert!(flags.is_empty());
322 }
323
324 #[test]
325 fn test_parse_wrangler_kv_commands() {
326 let cmd = "npx wrangler kv:namespace create NAMESPACE --preview";
327 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
328
329 assert!(full.contains("wrangler"));
330 assert_eq!(
331 args,
332 vec!["wrangler", "kv:namespace", "create", "NAMESPACE"]
333 );
334 assert!(flags.contains_key("preview"));
335 }
336
337 #[test]
338 fn test_parse_wrangler_pages_deploy() {
339 let cmd = "npx wrangler pages deploy ./dist --project-name my-project --branch main";
340 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
341
342 assert!(full.contains("wrangler"));
343 assert_eq!(args, vec!["wrangler", "pages", "deploy", "./dist"]);
344 assert_eq!(flags.get("project-name"), Some(&"my-project".to_string()));
345 assert_eq!(flags.get("branch"), Some(&"main".to_string()));
346 }
347
348 #[test]
349 fn test_parse_wrangler_secret_put() {
350 let cmd = "npx wrangler secret put API_KEY";
351 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
352
353 assert!(full.contains("wrangler"));
354 assert_eq!(args, vec!["wrangler", "secret", "put", "API_KEY"]);
355 assert!(flags.is_empty());
356 }
357
358 #[test]
359 fn test_parse_wrangler_in_pipeline() {
360 let cmd = "npm install && npx wrangler deploy --env production && npm test";
361 let (full, args, flags) = parse_command_context(cmd, 15).unwrap(); assert!(full.contains("wrangler"));
364 assert_eq!(args, vec!["wrangler", "deploy"]);
365 assert_eq!(flags.get("env"), Some(&"production".to_string()));
366 }
367
368 #[test]
369 fn test_parse_wrangler_with_output_redirect() {
370 let cmd = "npx wrangler deploy --env prod";
371 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
372
373 assert!(full.contains("wrangler"));
374 assert!(args.contains(&"wrangler".to_string()));
375 assert!(args.contains(&"deploy".to_string()));
376 assert_eq!(flags.get("env"), Some(&"prod".to_string()));
377 }
378
379 #[test]
380 fn test_parse_wrangler_init() {
381 let cmd = "bunx wrangler init my-worker --type rust";
382 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
383
384 assert!(full.contains("wrangler"));
385 assert_eq!(args, vec!["wrangler", "init", "my-worker"]);
386 assert_eq!(flags.get("type"), Some(&"rust".to_string()));
387 }
388
389 #[test]
390 fn test_parse_wrangler_whoami() {
391 let cmd = "npx wrangler whoami";
392 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
393
394 assert!(full.contains("wrangler"));
395 assert_eq!(args, vec!["wrangler", "whoami"]);
396 assert!(flags.is_empty());
397 }
398
399 #[test]
400 fn test_parse_wrangler_case_insensitive() {
401 let cmd = "NPX WRANGLER DEPLOY --ENV PRODUCTION";
402 let (full, args, flags) = parse_command_context(cmd, 0).unwrap();
403
404 assert!(full.to_lowercase().contains("wrangler"));
405 assert_eq!(args.len(), 2); assert!(!flags.is_empty());
407 }
408}