Skip to main content

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