tools.rs

  1use std::path::PathBuf;
  2
  3use agent_client_protocol as acp;
  4use agentic_coding_protocol as acp_old;
  5use itertools::Itertools;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use util::ResultExt;
  9
 10pub enum ClaudeTool {
 11    Task(Option<TaskToolParams>),
 12    NotebookRead(Option<NotebookReadToolParams>),
 13    NotebookEdit(Option<NotebookEditToolParams>),
 14    Edit(Option<EditToolParams>),
 15    MultiEdit(Option<MultiEditToolParams>),
 16    ReadFile(Option<ReadToolParams>),
 17    Write(Option<WriteToolParams>),
 18    Ls(Option<LsToolParams>),
 19    Glob(Option<GlobToolParams>),
 20    Grep(Option<GrepToolParams>),
 21    Terminal(Option<BashToolParams>),
 22    WebFetch(Option<WebFetchToolParams>),
 23    WebSearch(Option<WebSearchToolParams>),
 24    TodoWrite(Option<TodoWriteToolParams>),
 25    ExitPlanMode(Option<ExitPlanModeToolParams>),
 26    Other {
 27        name: String,
 28        input: serde_json::Value,
 29    },
 30}
 31
 32impl ClaudeTool {
 33    pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
 34        match tool_name {
 35            // Known tools
 36            "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
 37            "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
 38            "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
 39            "Write" => Self::Write(serde_json::from_value(input).log_err()),
 40            "LS" => Self::Ls(serde_json::from_value(input).log_err()),
 41            "Glob" => Self::Glob(serde_json::from_value(input).log_err()),
 42            "Grep" => Self::Grep(serde_json::from_value(input).log_err()),
 43            "Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
 44            "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
 45            "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
 46            "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
 47            "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
 48            "Task" => Self::Task(serde_json::from_value(input).log_err()),
 49            "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
 50            "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
 51            // Inferred from name
 52            _ => {
 53                let tool_name = tool_name.to_lowercase();
 54
 55                if tool_name.contains("edit") || tool_name.contains("write") {
 56                    Self::Edit(None)
 57                } else if tool_name.contains("terminal") {
 58                    Self::Terminal(None)
 59                } else {
 60                    Self::Other {
 61                        name: tool_name.to_string(),
 62                        input,
 63                    }
 64                }
 65            }
 66        }
 67    }
 68
 69    pub fn label(&self) -> String {
 70        match &self {
 71            Self::Task(Some(params)) => params.description.clone(),
 72            Self::Task(None) => "Task".into(),
 73            Self::NotebookRead(Some(params)) => {
 74                format!("Read Notebook {}", params.notebook_path.display())
 75            }
 76            Self::NotebookRead(None) => "Read Notebook".into(),
 77            Self::NotebookEdit(Some(params)) => {
 78                format!("Edit Notebook {}", params.notebook_path.display())
 79            }
 80            Self::NotebookEdit(None) => "Edit Notebook".into(),
 81            Self::Terminal(Some(params)) => format!("`{}`", params.command),
 82            Self::Terminal(None) => "Terminal".into(),
 83            Self::ReadFile(_) => "Read File".into(),
 84            Self::Ls(Some(params)) => {
 85                format!("List Directory {}", params.path.display())
 86            }
 87            Self::Ls(None) => "List Directory".into(),
 88            Self::Edit(Some(params)) => {
 89                format!("Edit {}", params.abs_path.display())
 90            }
 91            Self::Edit(None) => "Edit".into(),
 92            Self::MultiEdit(Some(params)) => {
 93                format!("Multi Edit {}", params.file_path.display())
 94            }
 95            Self::MultiEdit(None) => "Multi Edit".into(),
 96            Self::Write(Some(params)) => {
 97                format!("Write {}", params.file_path.display())
 98            }
 99            Self::Write(None) => "Write".into(),
100            Self::Glob(Some(params)) => {
101                format!("Glob `{params}`")
102            }
103            Self::Glob(None) => "Glob".into(),
104            Self::Grep(Some(params)) => format!("`{params}`"),
105            Self::Grep(None) => "Grep".into(),
106            Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
107            Self::WebFetch(None) => "Fetch".into(),
108            Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
109            Self::WebSearch(None) => "Web Search".into(),
110            Self::TodoWrite(Some(params)) => format!(
111                "Update TODOs: {}",
112                params.todos.iter().map(|todo| &todo.content).join(", ")
113            ),
114            Self::TodoWrite(None) => "Update TODOs".into(),
115            Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
116            Self::Other { name, .. } => name.clone(),
117        }
118    }
119    pub fn content(&self) -> Vec<acp::ToolCallContent> {
120        match &self {
121            Self::Other { input, .. } => vec![
122                format!(
123                    "```json\n{}```",
124                    serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
125                )
126                .into(),
127            ],
128            Self::Task(Some(params)) => vec![params.prompt.clone().into()],
129            Self::NotebookRead(Some(params)) => {
130                vec![params.notebook_path.display().to_string().into()]
131            }
132            Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
133            Self::Terminal(Some(params)) => vec![
134                format!(
135                    "`{}`\n\n{}",
136                    params.command,
137                    params.description.as_deref().unwrap_or_default()
138                )
139                .into(),
140            ],
141            Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
142            Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
143            Self::Glob(Some(params)) => vec![params.to_string().into()],
144            Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
145            Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
146            Self::WebSearch(Some(params)) => vec![params.to_string().into()],
147            Self::TodoWrite(Some(params)) => vec![
148                params
149                    .todos
150                    .iter()
151                    .map(|todo| {
152                        format!(
153                            "- {} {}: {}",
154                            match todo.status {
155                                TodoStatus::Completed => "",
156                                TodoStatus::InProgress => "🚧",
157                                TodoStatus::Pending => "",
158                            },
159                            todo.priority,
160                            todo.content
161                        )
162                    })
163                    .join("\n")
164                    .into(),
165            ],
166            Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
167            Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
168                diff: acp::Diff {
169                    path: params.abs_path.clone(),
170                    old_text: Some(params.old_text.clone()),
171                    new_text: params.new_text.clone(),
172                },
173            }],
174            Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
175                diff: acp::Diff {
176                    path: params.file_path.clone(),
177                    old_text: None,
178                    new_text: params.content.clone(),
179                },
180            }],
181            Self::MultiEdit(Some(params)) => {
182                // todo: show multiple edits in a multibuffer?
183                params
184                    .edits
185                    .first()
186                    .map(|edit| {
187                        vec![acp::ToolCallContent::Diff {
188                            diff: acp::Diff {
189                                path: params.file_path.clone(),
190                                old_text: Some(edit.old_string.clone()),
191                                new_text: edit.new_string.clone(),
192                            },
193                        }]
194                    })
195                    .unwrap_or_default()
196            }
197            Self::Task(None)
198            | Self::NotebookRead(None)
199            | Self::NotebookEdit(None)
200            | Self::Terminal(None)
201            | Self::ReadFile(None)
202            | Self::Ls(None)
203            | Self::Glob(None)
204            | Self::Grep(None)
205            | Self::WebFetch(None)
206            | Self::WebSearch(None)
207            | Self::TodoWrite(None)
208            | Self::ExitPlanMode(None)
209            | Self::Edit(None)
210            | Self::Write(None)
211            | Self::MultiEdit(None) => vec![],
212        }
213    }
214
215    pub fn kind(&self) -> acp::ToolKind {
216        match self {
217            Self::Task(_) => acp::ToolKind::Think,
218            Self::NotebookRead(_) => acp::ToolKind::Read,
219            Self::NotebookEdit(_) => acp::ToolKind::Edit,
220            Self::Edit(_) => acp::ToolKind::Edit,
221            Self::MultiEdit(_) => acp::ToolKind::Edit,
222            Self::Write(_) => acp::ToolKind::Edit,
223            Self::ReadFile(_) => acp::ToolKind::Read,
224            Self::Ls(_) => acp::ToolKind::Search,
225            Self::Glob(_) => acp::ToolKind::Search,
226            Self::Grep(_) => acp::ToolKind::Search,
227            Self::Terminal(_) => acp::ToolKind::Execute,
228            Self::WebSearch(_) => acp::ToolKind::Search,
229            Self::WebFetch(_) => acp::ToolKind::Fetch,
230            Self::TodoWrite(_) => acp::ToolKind::Think,
231            Self::ExitPlanMode(_) => acp::ToolKind::Think,
232            Self::Other { .. } => acp::ToolKind::Other,
233        }
234    }
235
236    pub fn confirmation(&self, description: Option<String>) -> acp_old::ToolCallConfirmation {
237        match &self {
238            Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
239                acp_old::ToolCallConfirmation::Edit { description }
240            }
241            Self::WebFetch(params) => acp_old::ToolCallConfirmation::Fetch {
242                urls: params
243                    .as_ref()
244                    .map(|p| vec![p.url.clone()])
245                    .unwrap_or_default(),
246                description,
247            },
248            Self::Terminal(Some(BashToolParams {
249                description,
250                command,
251                ..
252            })) => acp_old::ToolCallConfirmation::Execute {
253                command: command.clone(),
254                root_command: command.clone(),
255                description: description.clone(),
256            },
257            Self::ExitPlanMode(Some(params)) => acp_old::ToolCallConfirmation::Other {
258                description: if let Some(description) = description {
259                    format!("{description} {}", params.plan)
260                } else {
261                    params.plan.clone()
262                },
263            },
264            Self::Task(Some(params)) => acp_old::ToolCallConfirmation::Other {
265                description: if let Some(description) = description {
266                    format!("{description} {}", params.description)
267                } else {
268                    params.description.clone()
269                },
270            },
271            Self::Ls(Some(LsToolParams { path, .. }))
272            | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
273                let path = path.display();
274                acp_old::ToolCallConfirmation::Other {
275                    description: if let Some(description) = description {
276                        format!("{description} {path}")
277                    } else {
278                        path.to_string()
279                    },
280                }
281            }
282            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
283                let path = notebook_path.display();
284                acp_old::ToolCallConfirmation::Other {
285                    description: if let Some(description) = description {
286                        format!("{description} {path}")
287                    } else {
288                        path.to_string()
289                    },
290                }
291            }
292            Self::Glob(Some(params)) => acp_old::ToolCallConfirmation::Other {
293                description: if let Some(description) = description {
294                    format!("{description} {params}")
295                } else {
296                    params.to_string()
297                },
298            },
299            Self::Grep(Some(params)) => acp_old::ToolCallConfirmation::Other {
300                description: if let Some(description) = description {
301                    format!("{description} {params}")
302                } else {
303                    params.to_string()
304                },
305            },
306            Self::WebSearch(Some(params)) => acp_old::ToolCallConfirmation::Other {
307                description: if let Some(description) = description {
308                    format!("{description} {params}")
309                } else {
310                    params.to_string()
311                },
312            },
313            Self::TodoWrite(Some(params)) => {
314                let params = params.todos.iter().map(|todo| &todo.content).join(", ");
315                acp_old::ToolCallConfirmation::Other {
316                    description: if let Some(description) = description {
317                        format!("{description} {params}")
318                    } else {
319                        params
320                    },
321                }
322            }
323            Self::Terminal(None)
324            | Self::Task(None)
325            | Self::NotebookRead(None)
326            | Self::ExitPlanMode(None)
327            | Self::Ls(None)
328            | Self::Glob(None)
329            | Self::Grep(None)
330            | Self::ReadFile(None)
331            | Self::WebSearch(None)
332            | Self::TodoWrite(None)
333            | Self::Other { .. } => acp_old::ToolCallConfirmation::Other {
334                description: description.unwrap_or("".to_string()),
335            },
336        }
337    }
338
339    pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
340        match &self {
341            Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
342                path: abs_path.clone(),
343                line: None,
344            }],
345            Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
346                vec![acp::ToolCallLocation {
347                    path: file_path.clone(),
348                    line: None,
349                }]
350            }
351            Self::Write(Some(WriteToolParams { file_path, .. })) => {
352                vec![acp::ToolCallLocation {
353                    path: file_path.clone(),
354                    line: None,
355                }]
356            }
357            Self::ReadFile(Some(ReadToolParams {
358                abs_path, offset, ..
359            })) => vec![acp::ToolCallLocation {
360                path: abs_path.clone(),
361                line: *offset,
362            }],
363            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
364                vec![acp::ToolCallLocation {
365                    path: notebook_path.clone(),
366                    line: None,
367                }]
368            }
369            Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
370                vec![acp::ToolCallLocation {
371                    path: notebook_path.clone(),
372                    line: None,
373                }]
374            }
375            Self::Glob(Some(GlobToolParams {
376                path: Some(path), ..
377            })) => vec![acp::ToolCallLocation {
378                path: path.clone(),
379                line: None,
380            }],
381            Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
382                path: path.clone(),
383                line: None,
384            }],
385            Self::Grep(Some(GrepToolParams {
386                path: Some(path), ..
387            })) => vec![acp::ToolCallLocation {
388                path: PathBuf::from(path),
389                line: None,
390            }],
391            Self::Task(_)
392            | Self::NotebookRead(None)
393            | Self::NotebookEdit(None)
394            | Self::Edit(None)
395            | Self::MultiEdit(None)
396            | Self::Write(None)
397            | Self::ReadFile(None)
398            | Self::Ls(None)
399            | Self::Glob(_)
400            | Self::Grep(_)
401            | Self::Terminal(_)
402            | Self::WebFetch(_)
403            | Self::WebSearch(_)
404            | Self::TodoWrite(_)
405            | Self::ExitPlanMode(_)
406            | Self::Other { .. } => vec![],
407        }
408    }
409
410    pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
411        acp::ToolCall {
412            id,
413            kind: self.kind(),
414            status: acp::ToolCallStatus::InProgress,
415            label: self.label(),
416            content: self.content(),
417            locations: self.locations(),
418        }
419    }
420}
421
422#[derive(Deserialize, JsonSchema, Debug)]
423pub struct EditToolParams {
424    /// The absolute path to the file to read.
425    pub abs_path: PathBuf,
426    /// The old text to replace (must be unique in the file)
427    pub old_text: String,
428    /// The new text.
429    pub new_text: String,
430}
431
432#[derive(Deserialize, JsonSchema, Debug)]
433pub struct ReadToolParams {
434    /// The absolute path to the file to read.
435    pub abs_path: PathBuf,
436    /// Which line to start reading from. Omit to start from the beginning.
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub offset: Option<u32>,
439    /// How many lines to read. Omit for the whole file.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub limit: Option<u32>,
442}
443
444#[derive(Deserialize, JsonSchema, Debug)]
445pub struct WriteToolParams {
446    /// Absolute path for new file
447    pub file_path: PathBuf,
448    /// File content
449    pub content: String,
450}
451
452#[derive(Deserialize, JsonSchema, Debug)]
453pub struct BashToolParams {
454    /// Shell command to execute
455    pub command: String,
456    /// 5-10 word description of what command does
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub description: Option<String>,
459    /// Timeout in ms (max 600000ms/10min, default 120000ms)
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub timeout: Option<u32>,
462}
463
464#[derive(Deserialize, JsonSchema, Debug)]
465pub struct GlobToolParams {
466    /// Glob pattern like **/*.js or src/**/*.ts
467    pub pattern: String,
468    /// Directory to search in (omit for current directory)
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub path: Option<PathBuf>,
471}
472
473impl std::fmt::Display for GlobToolParams {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        if let Some(path) = &self.path {
476            write!(f, "{}", path.display())?;
477        }
478        write!(f, "{}", self.pattern)
479    }
480}
481
482#[derive(Deserialize, JsonSchema, Debug)]
483pub struct LsToolParams {
484    /// Absolute path to directory
485    pub path: PathBuf,
486    /// Array of glob patterns to ignore
487    #[serde(default, skip_serializing_if = "Vec::is_empty")]
488    pub ignore: Vec<String>,
489}
490
491#[derive(Deserialize, JsonSchema, Debug)]
492pub struct GrepToolParams {
493    /// Regex pattern to search for
494    pub pattern: String,
495    /// File/directory to search (defaults to current directory)
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub path: Option<String>,
498    /// "content" (shows lines), "files_with_matches" (default), "count"
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub output_mode: Option<GrepOutputMode>,
501    /// Filter files with glob pattern like "*.js"
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub glob: Option<String>,
504    /// File type filter like "js", "py", "rust"
505    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
506    pub file_type: Option<String>,
507    /// Case insensitive search
508    #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
509    pub case_insensitive: bool,
510    /// Show line numbers (content mode only)
511    #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
512    pub line_numbers: bool,
513    /// Lines after match (content mode only)
514    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
515    pub after_context: Option<u32>,
516    /// Lines before match (content mode only)
517    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
518    pub before_context: Option<u32>,
519    /// Lines before and after match (content mode only)
520    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
521    pub context: Option<u32>,
522    /// Enable multiline/cross-line matching
523    #[serde(default, skip_serializing_if = "is_false")]
524    pub multiline: bool,
525    /// Limit output to first N results
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub head_limit: Option<u32>,
528}
529
530impl std::fmt::Display for GrepToolParams {
531    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
532        write!(f, "grep")?;
533
534        // Boolean flags
535        if self.case_insensitive {
536            write!(f, " -i")?;
537        }
538        if self.line_numbers {
539            write!(f, " -n")?;
540        }
541
542        // Context options
543        if let Some(after) = self.after_context {
544            write!(f, " -A {}", after)?;
545        }
546        if let Some(before) = self.before_context {
547            write!(f, " -B {}", before)?;
548        }
549        if let Some(context) = self.context {
550            write!(f, " -C {}", context)?;
551        }
552
553        // Output mode
554        if let Some(mode) = &self.output_mode {
555            match mode {
556                GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
557                GrepOutputMode::Count => write!(f, " -c")?,
558                GrepOutputMode::Content => {} // Default mode
559            }
560        }
561
562        // Head limit
563        if let Some(limit) = self.head_limit {
564            write!(f, " | head -{}", limit)?;
565        }
566
567        // Glob pattern
568        if let Some(glob) = &self.glob {
569            write!(f, " --include=\"{}\"", glob)?;
570        }
571
572        // File type
573        if let Some(file_type) = &self.file_type {
574            write!(f, " --type={}", file_type)?;
575        }
576
577        // Multiline
578        if self.multiline {
579            write!(f, " -P")?; // Perl-compatible regex for multiline
580        }
581
582        // Pattern (escaped if contains special characters)
583        write!(f, " \"{}\"", self.pattern)?;
584
585        // Path
586        if let Some(path) = &self.path {
587            write!(f, " {}", path)?;
588        }
589
590        Ok(())
591    }
592}
593
594#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
595#[serde(rename_all = "snake_case")]
596pub enum TodoPriority {
597    High,
598    Medium,
599    Low,
600}
601
602impl Into<acp::PlanEntryPriority> for TodoPriority {
603    fn into(self) -> acp::PlanEntryPriority {
604        match self {
605            TodoPriority::High => acp::PlanEntryPriority::High,
606            TodoPriority::Medium => acp::PlanEntryPriority::Medium,
607            TodoPriority::Low => acp::PlanEntryPriority::Low,
608        }
609    }
610}
611
612#[derive(Deserialize, Serialize, JsonSchema, Debug)]
613#[serde(rename_all = "snake_case")]
614pub enum TodoStatus {
615    Pending,
616    InProgress,
617    Completed,
618}
619
620impl Into<acp::PlanEntryStatus> for TodoStatus {
621    fn into(self) -> acp::PlanEntryStatus {
622        match self {
623            TodoStatus::Pending => acp::PlanEntryStatus::Pending,
624            TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
625            TodoStatus::Completed => acp::PlanEntryStatus::Completed,
626        }
627    }
628}
629
630#[derive(Deserialize, Serialize, JsonSchema, Debug)]
631pub struct Todo {
632    /// Unique identifier
633    pub id: String,
634    /// Task description
635    pub content: String,
636    /// Priority level of the todo
637    pub priority: TodoPriority,
638    /// Current status of the todo
639    pub status: TodoStatus,
640}
641
642impl Into<acp::PlanEntry> for Todo {
643    fn into(self) -> acp::PlanEntry {
644        acp::PlanEntry {
645            content: self.content,
646            priority: self.priority.into(),
647            status: self.status.into(),
648        }
649    }
650}
651
652#[derive(Deserialize, JsonSchema, Debug)]
653pub struct TodoWriteToolParams {
654    pub todos: Vec<Todo>,
655}
656
657#[derive(Deserialize, JsonSchema, Debug)]
658pub struct ExitPlanModeToolParams {
659    /// Implementation plan in markdown format
660    pub plan: String,
661}
662
663#[derive(Deserialize, JsonSchema, Debug)]
664pub struct TaskToolParams {
665    /// Short 3-5 word description of task
666    pub description: String,
667    /// Detailed task for agent to perform
668    pub prompt: String,
669}
670
671#[derive(Deserialize, JsonSchema, Debug)]
672pub struct NotebookReadToolParams {
673    /// Absolute path to .ipynb file
674    pub notebook_path: PathBuf,
675    /// Specific cell ID to read
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub cell_id: Option<String>,
678}
679
680#[derive(Deserialize, Serialize, JsonSchema, Debug)]
681#[serde(rename_all = "snake_case")]
682pub enum CellType {
683    Code,
684    Markdown,
685}
686
687#[derive(Deserialize, Serialize, JsonSchema, Debug)]
688#[serde(rename_all = "snake_case")]
689pub enum EditMode {
690    Replace,
691    Insert,
692    Delete,
693}
694
695#[derive(Deserialize, JsonSchema, Debug)]
696pub struct NotebookEditToolParams {
697    /// Absolute path to .ipynb file
698    pub notebook_path: PathBuf,
699    /// New cell content
700    pub new_source: String,
701    /// Cell ID to edit
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub cell_id: Option<String>,
704    /// Type of cell (code or markdown)
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub cell_type: Option<CellType>,
707    /// Edit operation mode
708    #[serde(skip_serializing_if = "Option::is_none")]
709    pub edit_mode: Option<EditMode>,
710}
711
712#[derive(Deserialize, Serialize, JsonSchema, Debug)]
713pub struct MultiEditItem {
714    /// The text to search for and replace
715    pub old_string: String,
716    /// The replacement text
717    pub new_string: String,
718    /// Whether to replace all occurrences or just the first
719    #[serde(default, skip_serializing_if = "is_false")]
720    pub replace_all: bool,
721}
722
723#[derive(Deserialize, JsonSchema, Debug)]
724pub struct MultiEditToolParams {
725    /// Absolute path to file
726    pub file_path: PathBuf,
727    /// List of edits to apply
728    pub edits: Vec<MultiEditItem>,
729}
730
731fn is_false(v: &bool) -> bool {
732    !*v
733}
734
735#[derive(Deserialize, JsonSchema, Debug)]
736#[serde(rename_all = "snake_case")]
737pub enum GrepOutputMode {
738    Content,
739    FilesWithMatches,
740    Count,
741}
742
743#[derive(Deserialize, JsonSchema, Debug)]
744pub struct WebFetchToolParams {
745    /// Valid URL to fetch
746    #[serde(rename = "url")]
747    pub url: String,
748    /// What to extract from content
749    pub prompt: String,
750}
751
752#[derive(Deserialize, JsonSchema, Debug)]
753pub struct WebSearchToolParams {
754    /// Search query (min 2 chars)
755    pub query: String,
756    /// Only include these domains
757    #[serde(default, skip_serializing_if = "Vec::is_empty")]
758    pub allowed_domains: Vec<String>,
759    /// Exclude these domains
760    #[serde(default, skip_serializing_if = "Vec::is_empty")]
761    pub blocked_domains: Vec<String>,
762}
763
764impl std::fmt::Display for WebSearchToolParams {
765    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
766        write!(f, "\"{}\"", self.query)?;
767
768        if !self.allowed_domains.is_empty() {
769            write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
770        }
771
772        if !self.blocked_domains.is_empty() {
773            write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
774        }
775
776        Ok(())
777    }
778}