tools.rs

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