claude_codes/
tool_inputs.rs

1//! Typed tool input definitions for Claude Code tools.
2//!
3//! This module provides strongly-typed structs for the input parameters of each
4//! Claude Code tool. Using these types instead of raw `serde_json::Value` provides:
5//!
6//! - Compile-time type checking
7//! - IDE autocompletion and documentation
8//! - Self-documenting API
9//!
10//! # Example
11//!
12//! ```
13//! use claude_codes::{ToolInput, BashInput};
14//!
15//! // Parse a tool input from JSON
16//! let json = serde_json::json!({
17//!     "command": "ls -la",
18//!     "description": "List files in current directory"
19//! });
20//!
21//! let input: ToolInput = serde_json::from_value(json).unwrap();
22//! if let ToolInput::Bash(bash) = input {
23//!     assert_eq!(bash.command, "ls -la");
24//! }
25//! ```
26
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::collections::HashMap;
30
31// ============================================================================
32// Individual Tool Input Structs
33// ============================================================================
34
35/// Input for the Bash tool - executes shell commands.
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct BashInput {
38    /// The bash command to execute (required)
39    pub command: String,
40
41    /// Human-readable description of what the command does
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub description: Option<String>,
44
45    /// Timeout in milliseconds (max 600000)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub timeout: Option<u64>,
48
49    /// Whether to run the command in the background
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub run_in_background: Option<bool>,
52}
53
54/// Input for the Read tool - reads file contents.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct ReadInput {
57    /// The absolute path to the file to read
58    pub file_path: String,
59
60    /// The line number to start reading from (1-indexed)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub offset: Option<i64>,
63
64    /// The number of lines to read
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub limit: Option<i64>,
67}
68
69/// Input for the Write tool - writes content to a file.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct WriteInput {
72    /// The absolute path to the file to write
73    pub file_path: String,
74
75    /// The content to write to the file
76    pub content: String,
77}
78
79/// Input for the Edit tool - performs string replacements in files.
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct EditInput {
82    /// The absolute path to the file to modify
83    pub file_path: String,
84
85    /// The text to replace
86    pub old_string: String,
87
88    /// The text to replace it with
89    pub new_string: String,
90
91    /// Replace all occurrences of old_string (default false)
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub replace_all: Option<bool>,
94}
95
96/// Input for the Glob tool - finds files matching a pattern.
97///
98/// The `deny_unknown_fields` attribute ensures Glob only matches exact
99/// Glob inputs and doesn't accidentally match Grep inputs (which share
100/// the `pattern` field but have additional Grep-specific fields).
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102#[serde(deny_unknown_fields)]
103pub struct GlobInput {
104    /// The glob pattern to match files against (e.g., "**/*.rs")
105    pub pattern: String,
106
107    /// The directory to search in (defaults to current working directory)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub path: Option<String>,
110}
111
112/// Input for the Grep tool - searches file contents.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114pub struct GrepInput {
115    /// The regular expression pattern to search for
116    pub pattern: String,
117
118    /// File or directory to search in
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub path: Option<String>,
121
122    /// Glob pattern to filter files (e.g., "*.js")
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub glob: Option<String>,
125
126    /// File type to search (e.g., "js", "py", "rust")
127    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
128    pub file_type: Option<String>,
129
130    /// Case insensitive search
131    #[serde(rename = "-i", skip_serializing_if = "Option::is_none")]
132    pub case_insensitive: Option<bool>,
133
134    /// Show line numbers in output
135    #[serde(rename = "-n", skip_serializing_if = "Option::is_none")]
136    pub line_numbers: Option<bool>,
137
138    /// Number of lines to show after each match
139    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
140    pub after_context: Option<u32>,
141
142    /// Number of lines to show before each match
143    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
144    pub before_context: Option<u32>,
145
146    /// Number of lines to show before and after each match
147    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
148    pub context: Option<u32>,
149
150    /// Output mode: "content", "files_with_matches", or "count"
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub output_mode: Option<String>,
153
154    /// Enable multiline mode
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub multiline: Option<bool>,
157
158    /// Limit output to first N lines/entries
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub head_limit: Option<u32>,
161
162    /// Skip first N lines/entries
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub offset: Option<u32>,
165}
166
167/// Input for the Task tool - launches subagents.
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct TaskInput {
170    /// A short (3-5 word) description of the task
171    pub description: String,
172
173    /// The task for the agent to perform
174    pub prompt: String,
175
176    /// The type of specialized agent to use
177    pub subagent_type: String,
178
179    /// Whether to run the agent in the background
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub run_in_background: Option<bool>,
182
183    /// Optional model to use for this agent
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub model: Option<String>,
186
187    /// Maximum number of agentic turns
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub max_turns: Option<u32>,
190
191    /// Optional agent ID to resume from
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub resume: Option<String>,
194}
195
196/// Input for the WebFetch tool - fetches and processes web content.
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198pub struct WebFetchInput {
199    /// The URL to fetch content from
200    pub url: String,
201
202    /// The prompt to run on the fetched content
203    pub prompt: String,
204}
205
206/// Input for the WebSearch tool - searches the web.
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
208pub struct WebSearchInput {
209    /// The search query to use
210    pub query: String,
211
212    /// Only include search results from these domains
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub allowed_domains: Option<Vec<String>>,
215
216    /// Never include search results from these domains
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub blocked_domains: Option<Vec<String>>,
219}
220
221/// Input for the TodoWrite tool - manages task lists.
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223pub struct TodoWriteInput {
224    /// The updated todo list
225    pub todos: Vec<TodoItem>,
226}
227
228/// A single todo item in a task list.
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct TodoItem {
231    /// The task description (imperative form)
232    pub content: String,
233
234    /// Current status: "pending", "in_progress", or "completed"
235    pub status: String,
236
237    /// The present continuous form shown during execution
238    #[serde(rename = "activeForm")]
239    pub active_form: String,
240}
241
242/// Input for the AskUserQuestion tool - asks the user questions.
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub struct AskUserQuestionInput {
245    /// Questions to ask the user (1-4 questions)
246    pub questions: Vec<Question>,
247
248    /// User answers collected by the permission component
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub answers: Option<HashMap<String, String>>,
251
252    /// Optional metadata for tracking and analytics
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub metadata: Option<QuestionMetadata>,
255}
256
257/// A question to ask the user.
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct Question {
260    /// The complete question to ask the user
261    pub question: String,
262
263    /// Very short label displayed as a chip/tag (max 12 chars)
264    pub header: String,
265
266    /// The available choices for this question (2-4 options)
267    pub options: Vec<QuestionOption>,
268
269    /// Whether multiple options can be selected
270    #[serde(rename = "multiSelect", default)]
271    pub multi_select: bool,
272}
273
274/// An option for a question.
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct QuestionOption {
277    /// The display text for this option
278    pub label: String,
279
280    /// Explanation of what this option means
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub description: Option<String>,
283}
284
285/// Metadata for questions.
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct QuestionMetadata {
288    /// Optional identifier for the source of this question
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub source: Option<String>,
291}
292
293/// Input for the NotebookEdit tool - edits Jupyter notebooks.
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
295pub struct NotebookEditInput {
296    /// The absolute path to the Jupyter notebook file
297    pub notebook_path: String,
298
299    /// The new source for the cell
300    pub new_source: String,
301
302    /// The ID of the cell to edit
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub cell_id: Option<String>,
305
306    /// The type of the cell (code or markdown)
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub cell_type: Option<String>,
309
310    /// The type of edit to make (replace, insert, delete)
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub edit_mode: Option<String>,
313}
314
315/// Input for the TaskOutput tool - retrieves output from background tasks.
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct TaskOutputInput {
318    /// The task ID to get output from
319    pub task_id: String,
320
321    /// Whether to wait for completion (default true)
322    #[serde(default = "default_true")]
323    pub block: bool,
324
325    /// Max wait time in ms (default 30000, max 600000)
326    #[serde(default = "default_timeout")]
327    pub timeout: u64,
328}
329
330fn default_true() -> bool {
331    true
332}
333
334fn default_timeout() -> u64 {
335    30000
336}
337
338/// Input for the KillShell tool - kills a running background shell.
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340pub struct KillShellInput {
341    /// The ID of the background shell to kill
342    pub shell_id: String,
343}
344
345/// Input for the Skill tool - executes a skill.
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347pub struct SkillInput {
348    /// The skill name (e.g., "commit", "review-pr")
349    pub skill: String,
350
351    /// Optional arguments for the skill
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub args: Option<String>,
354}
355
356/// Input for the EnterPlanMode tool - enters planning mode.
357///
358/// This is an empty struct as EnterPlanMode takes no parameters.
359/// The `deny_unknown_fields` attribute ensures this only matches
360/// empty JSON objects `{}`, not arbitrary JSON.
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
362#[serde(deny_unknown_fields)]
363pub struct EnterPlanModeInput {}
364
365/// Input for the ExitPlanMode tool - exits planning mode.
366///
367/// The `deny_unknown_fields` attribute ensures this only matches JSON objects
368/// that contain known fields (or are empty), not arbitrary JSON.
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
370#[serde(deny_unknown_fields)]
371pub struct ExitPlanModeInput {
372    /// Prompt-based permissions needed to implement the plan
373    #[serde(rename = "allowedPrompts", skip_serializing_if = "Option::is_none")]
374    pub allowed_prompts: Option<Vec<AllowedPrompt>>,
375
376    /// Whether to push the plan to a remote Claude.ai session
377    #[serde(rename = "pushToRemote", skip_serializing_if = "Option::is_none")]
378    pub push_to_remote: Option<bool>,
379
380    /// The remote session ID if pushed to remote
381    #[serde(rename = "remoteSessionId", skip_serializing_if = "Option::is_none")]
382    pub remote_session_id: Option<String>,
383
384    /// The remote session URL if pushed to remote
385    #[serde(rename = "remoteSessionUrl", skip_serializing_if = "Option::is_none")]
386    pub remote_session_url: Option<String>,
387}
388
389/// An allowed prompt permission for plan mode.
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
391pub struct AllowedPrompt {
392    /// The tool this prompt applies to
393    pub tool: String,
394
395    /// Semantic description of the action
396    pub prompt: String,
397}
398
399// ============================================================================
400// ToolInput Enum - Unified type for all tool inputs
401// ============================================================================
402
403/// Unified enum representing input for any Claude Code tool.
404///
405/// This enum uses `#[serde(untagged)]` to automatically deserialize based on
406/// the structure of the JSON. The `Unknown` variant serves as a fallback for:
407/// - New tools added in future Claude CLI versions
408/// - Custom MCP tools provided by users
409/// - Any tool input that doesn't match known schemas
410///
411/// # Example
412///
413/// ```
414/// use claude_codes::ToolInput;
415///
416/// // Known tool - deserializes to specific variant
417/// let bash_json = serde_json::json!({"command": "ls"});
418/// let input: ToolInput = serde_json::from_value(bash_json).unwrap();
419/// assert!(matches!(input, ToolInput::Bash(_)));
420///
421/// // Unknown tool - falls back to Unknown variant
422/// let custom_json = serde_json::json!({"custom_field": "value"});
423/// let input: ToolInput = serde_json::from_value(custom_json).unwrap();
424/// assert!(matches!(input, ToolInput::Unknown(_)));
425/// ```
426///
427/// # Note on Ordering
428///
429/// The variants are ordered from most specific (most required fields) to least
430/// specific to ensure correct deserialization with `#[serde(untagged)]`.
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432#[serde(untagged)]
433pub enum ToolInput {
434    /// Edit tool - has unique field combination (file_path, old_string, new_string)
435    Edit(EditInput),
436
437    /// Write tool - file_path + content
438    Write(WriteInput),
439
440    /// AskUserQuestion tool - has questions array
441    AskUserQuestion(AskUserQuestionInput),
442
443    /// TodoWrite tool - has todos array
444    TodoWrite(TodoWriteInput),
445
446    /// Task tool - description + prompt + subagent_type
447    Task(TaskInput),
448
449    /// NotebookEdit tool - notebook_path + new_source
450    NotebookEdit(NotebookEditInput),
451
452    /// WebFetch tool - url + prompt
453    WebFetch(WebFetchInput),
454
455    /// TaskOutput tool - task_id + block + timeout
456    TaskOutput(TaskOutputInput),
457
458    /// Bash tool - has command field
459    Bash(BashInput),
460
461    /// Read tool - has file_path
462    Read(ReadInput),
463
464    /// Glob tool - has pattern field (with deny_unknown_fields, must come before Grep)
465    Glob(GlobInput),
466
467    /// Grep tool - has pattern field plus many optional fields
468    Grep(GrepInput),
469
470    /// WebSearch tool - has query field
471    WebSearch(WebSearchInput),
472
473    /// KillShell tool - has shell_id
474    KillShell(KillShellInput),
475
476    /// Skill tool - has skill field
477    Skill(SkillInput),
478
479    /// ExitPlanMode tool
480    ExitPlanMode(ExitPlanModeInput),
481
482    /// EnterPlanMode tool (empty input)
483    EnterPlanMode(EnterPlanModeInput),
484
485    /// Unknown tool input - fallback for custom/new tools
486    ///
487    /// This variant captures any tool input that doesn't match the known schemas.
488    /// Use this for:
489    /// - MCP tools provided by users
490    /// - New tools in future Claude CLI versions
491    /// - Any custom tool integration
492    Unknown(Value),
493}
494
495impl ToolInput {
496    /// Returns the tool name if it can be determined from the input type.
497    ///
498    /// For `Unknown` variants, returns `None` since the tool name cannot be
499    /// determined from the input structure alone.
500    pub fn tool_name(&self) -> Option<&'static str> {
501        match self {
502            ToolInput::Bash(_) => Some("Bash"),
503            ToolInput::Read(_) => Some("Read"),
504            ToolInput::Write(_) => Some("Write"),
505            ToolInput::Edit(_) => Some("Edit"),
506            ToolInput::Glob(_) => Some("Glob"),
507            ToolInput::Grep(_) => Some("Grep"),
508            ToolInput::Task(_) => Some("Task"),
509            ToolInput::WebFetch(_) => Some("WebFetch"),
510            ToolInput::WebSearch(_) => Some("WebSearch"),
511            ToolInput::TodoWrite(_) => Some("TodoWrite"),
512            ToolInput::AskUserQuestion(_) => Some("AskUserQuestion"),
513            ToolInput::NotebookEdit(_) => Some("NotebookEdit"),
514            ToolInput::TaskOutput(_) => Some("TaskOutput"),
515            ToolInput::KillShell(_) => Some("KillShell"),
516            ToolInput::Skill(_) => Some("Skill"),
517            ToolInput::EnterPlanMode(_) => Some("EnterPlanMode"),
518            ToolInput::ExitPlanMode(_) => Some("ExitPlanMode"),
519            ToolInput::Unknown(_) => None,
520        }
521    }
522
523    /// Try to get the input as a Bash input.
524    pub fn as_bash(&self) -> Option<&BashInput> {
525        match self {
526            ToolInput::Bash(input) => Some(input),
527            _ => None,
528        }
529    }
530
531    /// Try to get the input as a Read input.
532    pub fn as_read(&self) -> Option<&ReadInput> {
533        match self {
534            ToolInput::Read(input) => Some(input),
535            _ => None,
536        }
537    }
538
539    /// Try to get the input as a Write input.
540    pub fn as_write(&self) -> Option<&WriteInput> {
541        match self {
542            ToolInput::Write(input) => Some(input),
543            _ => None,
544        }
545    }
546
547    /// Try to get the input as an Edit input.
548    pub fn as_edit(&self) -> Option<&EditInput> {
549        match self {
550            ToolInput::Edit(input) => Some(input),
551            _ => None,
552        }
553    }
554
555    /// Try to get the input as a Glob input.
556    pub fn as_glob(&self) -> Option<&GlobInput> {
557        match self {
558            ToolInput::Glob(input) => Some(input),
559            _ => None,
560        }
561    }
562
563    /// Try to get the input as a Grep input.
564    pub fn as_grep(&self) -> Option<&GrepInput> {
565        match self {
566            ToolInput::Grep(input) => Some(input),
567            _ => None,
568        }
569    }
570
571    /// Try to get the input as a Task input.
572    pub fn as_task(&self) -> Option<&TaskInput> {
573        match self {
574            ToolInput::Task(input) => Some(input),
575            _ => None,
576        }
577    }
578
579    /// Try to get the input as a WebFetch input.
580    pub fn as_web_fetch(&self) -> Option<&WebFetchInput> {
581        match self {
582            ToolInput::WebFetch(input) => Some(input),
583            _ => None,
584        }
585    }
586
587    /// Try to get the input as a WebSearch input.
588    pub fn as_web_search(&self) -> Option<&WebSearchInput> {
589        match self {
590            ToolInput::WebSearch(input) => Some(input),
591            _ => None,
592        }
593    }
594
595    /// Try to get the input as a TodoWrite input.
596    pub fn as_todo_write(&self) -> Option<&TodoWriteInput> {
597        match self {
598            ToolInput::TodoWrite(input) => Some(input),
599            _ => None,
600        }
601    }
602
603    /// Try to get the input as an AskUserQuestion input.
604    pub fn as_ask_user_question(&self) -> Option<&AskUserQuestionInput> {
605        match self {
606            ToolInput::AskUserQuestion(input) => Some(input),
607            _ => None,
608        }
609    }
610
611    /// Try to get the input as a NotebookEdit input.
612    pub fn as_notebook_edit(&self) -> Option<&NotebookEditInput> {
613        match self {
614            ToolInput::NotebookEdit(input) => Some(input),
615            _ => None,
616        }
617    }
618
619    /// Try to get the input as a TaskOutput input.
620    pub fn as_task_output(&self) -> Option<&TaskOutputInput> {
621        match self {
622            ToolInput::TaskOutput(input) => Some(input),
623            _ => None,
624        }
625    }
626
627    /// Try to get the input as a KillShell input.
628    pub fn as_kill_shell(&self) -> Option<&KillShellInput> {
629        match self {
630            ToolInput::KillShell(input) => Some(input),
631            _ => None,
632        }
633    }
634
635    /// Try to get the input as a Skill input.
636    pub fn as_skill(&self) -> Option<&SkillInput> {
637        match self {
638            ToolInput::Skill(input) => Some(input),
639            _ => None,
640        }
641    }
642
643    /// Try to get the input as an unknown Value.
644    pub fn as_unknown(&self) -> Option<&Value> {
645        match self {
646            ToolInput::Unknown(value) => Some(value),
647            _ => None,
648        }
649    }
650
651    /// Check if this is an unknown tool input.
652    pub fn is_unknown(&self) -> bool {
653        matches!(self, ToolInput::Unknown(_))
654    }
655}
656
657// ============================================================================
658// Conversion implementations
659// ============================================================================
660
661impl From<BashInput> for ToolInput {
662    fn from(input: BashInput) -> Self {
663        ToolInput::Bash(input)
664    }
665}
666
667impl From<ReadInput> for ToolInput {
668    fn from(input: ReadInput) -> Self {
669        ToolInput::Read(input)
670    }
671}
672
673impl From<WriteInput> for ToolInput {
674    fn from(input: WriteInput) -> Self {
675        ToolInput::Write(input)
676    }
677}
678
679impl From<EditInput> for ToolInput {
680    fn from(input: EditInput) -> Self {
681        ToolInput::Edit(input)
682    }
683}
684
685impl From<GlobInput> for ToolInput {
686    fn from(input: GlobInput) -> Self {
687        ToolInput::Glob(input)
688    }
689}
690
691impl From<GrepInput> for ToolInput {
692    fn from(input: GrepInput) -> Self {
693        ToolInput::Grep(input)
694    }
695}
696
697impl From<TaskInput> for ToolInput {
698    fn from(input: TaskInput) -> Self {
699        ToolInput::Task(input)
700    }
701}
702
703impl From<WebFetchInput> for ToolInput {
704    fn from(input: WebFetchInput) -> Self {
705        ToolInput::WebFetch(input)
706    }
707}
708
709impl From<WebSearchInput> for ToolInput {
710    fn from(input: WebSearchInput) -> Self {
711        ToolInput::WebSearch(input)
712    }
713}
714
715impl From<TodoWriteInput> for ToolInput {
716    fn from(input: TodoWriteInput) -> Self {
717        ToolInput::TodoWrite(input)
718    }
719}
720
721impl From<AskUserQuestionInput> for ToolInput {
722    fn from(input: AskUserQuestionInput) -> Self {
723        ToolInput::AskUserQuestion(input)
724    }
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn test_bash_input_parsing() {
733        let json = serde_json::json!({
734            "command": "ls -la",
735            "description": "List files",
736            "timeout": 5000,
737            "run_in_background": false
738        });
739
740        let input: BashInput = serde_json::from_value(json).unwrap();
741        assert_eq!(input.command, "ls -la");
742        assert_eq!(input.description, Some("List files".to_string()));
743        assert_eq!(input.timeout, Some(5000));
744        assert_eq!(input.run_in_background, Some(false));
745    }
746
747    #[test]
748    fn test_bash_input_minimal() {
749        let json = serde_json::json!({
750            "command": "echo hello"
751        });
752
753        let input: BashInput = serde_json::from_value(json).unwrap();
754        assert_eq!(input.command, "echo hello");
755        assert_eq!(input.description, None);
756        assert_eq!(input.timeout, None);
757    }
758
759    #[test]
760    fn test_read_input_parsing() {
761        let json = serde_json::json!({
762            "file_path": "/home/user/test.rs",
763            "offset": 10,
764            "limit": 100
765        });
766
767        let input: ReadInput = serde_json::from_value(json).unwrap();
768        assert_eq!(input.file_path, "/home/user/test.rs");
769        assert_eq!(input.offset, Some(10));
770        assert_eq!(input.limit, Some(100));
771    }
772
773    #[test]
774    fn test_write_input_parsing() {
775        let json = serde_json::json!({
776            "file_path": "/tmp/test.txt",
777            "content": "Hello, world!"
778        });
779
780        let input: WriteInput = serde_json::from_value(json).unwrap();
781        assert_eq!(input.file_path, "/tmp/test.txt");
782        assert_eq!(input.content, "Hello, world!");
783    }
784
785    #[test]
786    fn test_edit_input_parsing() {
787        let json = serde_json::json!({
788            "file_path": "/home/user/code.rs",
789            "old_string": "fn old()",
790            "new_string": "fn new()",
791            "replace_all": true
792        });
793
794        let input: EditInput = serde_json::from_value(json).unwrap();
795        assert_eq!(input.file_path, "/home/user/code.rs");
796        assert_eq!(input.old_string, "fn old()");
797        assert_eq!(input.new_string, "fn new()");
798        assert_eq!(input.replace_all, Some(true));
799    }
800
801    #[test]
802    fn test_glob_input_parsing() {
803        let json = serde_json::json!({
804            "pattern": "**/*.rs",
805            "path": "/home/user/project"
806        });
807
808        let input: GlobInput = serde_json::from_value(json).unwrap();
809        assert_eq!(input.pattern, "**/*.rs");
810        assert_eq!(input.path, Some("/home/user/project".to_string()));
811    }
812
813    #[test]
814    fn test_grep_input_parsing() {
815        let json = serde_json::json!({
816            "pattern": "fn\\s+\\w+",
817            "path": "/home/user/project",
818            "type": "rust",
819            "-i": true,
820            "-C": 3
821        });
822
823        let input: GrepInput = serde_json::from_value(json).unwrap();
824        assert_eq!(input.pattern, "fn\\s+\\w+");
825        assert_eq!(input.file_type, Some("rust".to_string()));
826        assert_eq!(input.case_insensitive, Some(true));
827        assert_eq!(input.context, Some(3));
828    }
829
830    #[test]
831    fn test_task_input_parsing() {
832        let json = serde_json::json!({
833            "description": "Search codebase",
834            "prompt": "Find all usages of foo()",
835            "subagent_type": "Explore",
836            "run_in_background": true
837        });
838
839        let input: TaskInput = serde_json::from_value(json).unwrap();
840        assert_eq!(input.description, "Search codebase");
841        assert_eq!(input.prompt, "Find all usages of foo()");
842        assert_eq!(input.subagent_type, "Explore");
843        assert_eq!(input.run_in_background, Some(true));
844    }
845
846    #[test]
847    fn test_web_fetch_input_parsing() {
848        let json = serde_json::json!({
849            "url": "https://example.com",
850            "prompt": "Extract the main content"
851        });
852
853        let input: WebFetchInput = serde_json::from_value(json).unwrap();
854        assert_eq!(input.url, "https://example.com");
855        assert_eq!(input.prompt, "Extract the main content");
856    }
857
858    #[test]
859    fn test_web_search_input_parsing() {
860        let json = serde_json::json!({
861            "query": "rust serde tutorial",
862            "allowed_domains": ["docs.rs", "crates.io"]
863        });
864
865        let input: WebSearchInput = serde_json::from_value(json).unwrap();
866        assert_eq!(input.query, "rust serde tutorial");
867        assert_eq!(
868            input.allowed_domains,
869            Some(vec!["docs.rs".to_string(), "crates.io".to_string()])
870        );
871    }
872
873    #[test]
874    fn test_todo_write_input_parsing() {
875        let json = serde_json::json!({
876            "todos": [
877                {
878                    "content": "Fix the bug",
879                    "status": "in_progress",
880                    "activeForm": "Fixing the bug"
881                },
882                {
883                    "content": "Write tests",
884                    "status": "pending",
885                    "activeForm": "Writing tests"
886                }
887            ]
888        });
889
890        let input: TodoWriteInput = serde_json::from_value(json).unwrap();
891        assert_eq!(input.todos.len(), 2);
892        assert_eq!(input.todos[0].content, "Fix the bug");
893        assert_eq!(input.todos[0].status, "in_progress");
894        assert_eq!(input.todos[1].status, "pending");
895    }
896
897    #[test]
898    fn test_ask_user_question_input_parsing() {
899        let json = serde_json::json!({
900            "questions": [
901                {
902                    "question": "Which framework?",
903                    "header": "Framework",
904                    "options": [
905                        {"label": "React", "description": "Popular UI library"},
906                        {"label": "Vue", "description": "Progressive framework"}
907                    ],
908                    "multiSelect": false
909                }
910            ]
911        });
912
913        let input: AskUserQuestionInput = serde_json::from_value(json).unwrap();
914        assert_eq!(input.questions.len(), 1);
915        assert_eq!(input.questions[0].question, "Which framework?");
916        assert_eq!(input.questions[0].options.len(), 2);
917        assert_eq!(input.questions[0].options[0].label, "React");
918    }
919
920    #[test]
921    fn test_tool_input_enum_bash() {
922        let json = serde_json::json!({
923            "command": "ls -la"
924        });
925
926        let input: ToolInput = serde_json::from_value(json).unwrap();
927        assert!(matches!(input, ToolInput::Bash(_)));
928        assert_eq!(input.tool_name(), Some("Bash"));
929        assert!(input.as_bash().is_some());
930    }
931
932    #[test]
933    fn test_tool_input_enum_edit() {
934        let json = serde_json::json!({
935            "file_path": "/test.rs",
936            "old_string": "old",
937            "new_string": "new"
938        });
939
940        let input: ToolInput = serde_json::from_value(json).unwrap();
941        assert!(matches!(input, ToolInput::Edit(_)));
942        assert_eq!(input.tool_name(), Some("Edit"));
943    }
944
945    #[test]
946    fn test_tool_input_enum_unknown() {
947        // Custom MCP tool with unknown structure
948        let json = serde_json::json!({
949            "custom_field": "custom_value",
950            "another_field": 42
951        });
952
953        let input: ToolInput = serde_json::from_value(json).unwrap();
954        assert!(matches!(input, ToolInput::Unknown(_)));
955        assert_eq!(input.tool_name(), None);
956        assert!(input.is_unknown());
957
958        let unknown = input.as_unknown().unwrap();
959        assert_eq!(unknown.get("custom_field").unwrap(), "custom_value");
960    }
961
962    #[test]
963    fn test_tool_input_roundtrip() {
964        let original = BashInput {
965            command: "echo test".to_string(),
966            description: Some("Test command".to_string()),
967            timeout: Some(5000),
968            run_in_background: None,
969        };
970
971        let tool_input: ToolInput = original.clone().into();
972        let json = serde_json::to_value(&tool_input).unwrap();
973        let parsed: ToolInput = serde_json::from_value(json).unwrap();
974
975        if let ToolInput::Bash(bash) = parsed {
976            assert_eq!(bash.command, original.command);
977            assert_eq!(bash.description, original.description);
978        } else {
979            panic!("Expected Bash variant");
980        }
981    }
982
983    #[test]
984    fn test_notebook_edit_input_parsing() {
985        let json = serde_json::json!({
986            "notebook_path": "/home/user/notebook.ipynb",
987            "new_source": "print('hello')",
988            "cell_id": "abc123",
989            "cell_type": "code",
990            "edit_mode": "replace"
991        });
992
993        let input: NotebookEditInput = serde_json::from_value(json).unwrap();
994        assert_eq!(input.notebook_path, "/home/user/notebook.ipynb");
995        assert_eq!(input.new_source, "print('hello')");
996        assert_eq!(input.cell_id, Some("abc123".to_string()));
997    }
998
999    #[test]
1000    fn test_task_output_input_parsing() {
1001        let json = serde_json::json!({
1002            "task_id": "task-123",
1003            "block": false,
1004            "timeout": 60000
1005        });
1006
1007        let input: TaskOutputInput = serde_json::from_value(json).unwrap();
1008        assert_eq!(input.task_id, "task-123");
1009        assert!(!input.block);
1010        assert_eq!(input.timeout, 60000);
1011    }
1012
1013    #[test]
1014    fn test_skill_input_parsing() {
1015        let json = serde_json::json!({
1016            "skill": "commit",
1017            "args": "-m 'Fix bug'"
1018        });
1019
1020        let input: SkillInput = serde_json::from_value(json).unwrap();
1021        assert_eq!(input.skill, "commit");
1022        assert_eq!(input.args, Some("-m 'Fix bug'".to_string()));
1023    }
1024}