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}