Skip to main content

aether_cli/agent/new_agent_wizard/
draft_agent_entry.rs

1use aether_project::{AetherSettings, AgentConfig, McpSourceSpec, PromptSource};
2use std::{
3    fs::{create_dir_all, read_to_string, write},
4    path::{Path, PathBuf},
5};
6
7use super::new_agent_step::{NewAgentMode, PromptFile};
8use crate::error::CliError;
9
10pub struct DraftAgentEntry {
11    pub entry: AgentConfig,
12    pub system_md_content: String,
13    pub system_md_edited: bool,
14    pub selected_mcp_servers: Vec<String>,
15    pub workspace_mcp_configs: Vec<String>,
16}
17
18impl DraftAgentEntry {
19    pub fn slug(&self) -> String {
20        self.entry.name.to_lowercase().replace(' ', "-")
21    }
22
23    pub fn generated_paths(&self, mode: &NewAgentMode) -> GeneratedPaths {
24        let filename = format!("{}.md", self.slug().to_uppercase());
25        match mode {
26            NewAgentMode::ScaffoldProject => GeneratedPaths {
27                system_md: PathBuf::from(format!(".aether/{filename}")),
28                mcp_json: PathBuf::from(".aether/mcp.json"),
29            },
30            NewAgentMode::AddAgentToExistingProject => {
31                let slug = self.slug();
32                GeneratedPaths {
33                    system_md: PathBuf::from(format!(".aether/agents/{slug}/{filename}")),
34                    mcp_json: PathBuf::from(format!(".aether/agents/{slug}/mcp.json")),
35                }
36            }
37        }
38    }
39
40    pub fn to_agent_config(&self, mode: &NewAgentMode, inherited_prompts: &[String]) -> AgentConfig {
41        let paths = self.generated_paths(mode);
42
43        let mut prompts = vec![PromptSource::file(paths.system_md.to_string_lossy())];
44        match mode {
45            NewAgentMode::ScaffoldProject => {
46                prompts.extend(self.entry.prompts.iter().cloned());
47            }
48            NewAgentMode::AddAgentToExistingProject => {
49                for prompt in &self.entry.prompts {
50                    if let Some(path) = prompt.path()
51                        && !inherited_prompts.iter().any(|d| d == path)
52                    {
53                        prompts.push(prompt.clone());
54                    }
55                }
56            }
57        }
58
59        let mut mcp = if self.selected_mcp_servers.is_empty() {
60            vec![]
61        } else {
62            vec![McpSourceSpec::file(paths.mcp_json.to_string_lossy())]
63        };
64
65        mcp.extend(self.workspace_mcp_configs.iter().map(McpSourceSpec::file));
66
67        AgentConfig { prompts, mcps: mcp, ..self.entry.clone() }
68    }
69
70    pub fn to_settings(&self, mode: &NewAgentMode, existing: Option<&str>) -> AetherSettings {
71        match mode {
72            NewAgentMode::ScaffoldProject => {
73                let entry = self.to_agent_config(mode, &[]);
74                AetherSettings { agent: Some(entry.name.clone()), agents: vec![entry], ..AetherSettings::default() }
75            }
76            NewAgentMode::AddAgentToExistingProject => {
77                let inherited = inherited_prompts_from_existing(existing);
78                let entry = self.to_agent_config(mode, &inherited);
79
80                let mut config: AetherSettings =
81                    existing.and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default();
82                config.agents.push(entry);
83                config
84            }
85        }
86    }
87
88    pub fn to_mcp_json(&self) -> String {
89        use mcp_utils::client::config::{RawMcpConfig, RawMcpServerConfig};
90        use std::collections::BTreeMap;
91
92        let servers = self
93            .selected_mcp_servers
94            .iter()
95            .map(|entry| {
96                let name = entry.as_str();
97                let args = match name {
98                    "coding" => vec!["--rules-dir".into(), ".aether/skills".into()],
99                    "skills" => {
100                        vec!["--dir".into(), ".aether/skills".into(), "--notes-dir".into(), ".aether/notes".into()]
101                    }
102                    _ => vec![],
103                };
104                (name.to_string(), RawMcpServerConfig::InMemory { args, input: None })
105            })
106            .collect::<BTreeMap<_, _>>();
107
108        let config = RawMcpConfig { servers };
109        serde_json::to_string_pretty(&config).expect("mcp serialization cannot fail")
110    }
111}
112
113pub struct GeneratedPaths {
114    pub system_md: PathBuf,
115    pub mcp_json: PathBuf,
116}
117
118fn inherited_prompts_from_existing(existing: Option<&str>) -> Vec<String> {
119    existing
120        .and_then(|s| serde_json::from_str::<AetherSettings>(s).ok())
121        .map(|s| {
122            let prompts = if s.prompts.is_empty() {
123                s.agents.first().map(|agent| agent.prompts.clone()).unwrap_or_default()
124            } else {
125                s.prompts
126            };
127
128            prompts
129                .iter()
130                .filter_map(|p| p.path().map(str::to_string))
131                .filter(|p| PromptFile::all().iter().any(|d| d.filename() == p))
132                .collect()
133        })
134        .unwrap_or_default()
135}
136
137pub fn build_system_md(draft: &DraftAgentEntry) -> String {
138    format!(
139        "# {name}
140
141{description}
142
143## System Env
144
145Working directory: !`pwd`\\
146Platform: !`uname -s`\\
147Today's date: !`date +%Y-%m-%d`\\
148Git branch: !`git rev-parse --abbrev-ref HEAD`
149",
150        name = draft.entry.name,
151        description = draft.entry.description,
152    )
153}
154
155pub fn build_agents_md(draft: &DraftAgentEntry) -> String {
156    format!("# {}\n\n{}\n\nYou are an expert coding assistant.\n", draft.entry.name, draft.entry.description)
157}
158
159pub fn scaffold(project_root: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
160    create_dir_all(project_root).map_err(CliError::IoError)?;
161
162    let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
163    write_if_absent(&project_root.join(&paths.system_md), &draft.system_md_content)?;
164    write_if_absent(&project_root.join(".aether/mcp.json"), &draft.to_mcp_json())?;
165    if draft.entry.prompts.iter().any(|n| n.path() == Some(PromptFile::Agents.filename())) {
166        write_if_absent(&project_root.join("AGENTS.md"), &build_agents_md(draft))?;
167    }
168    let config = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
169    let json = serde_json::to_string_pretty(&config).expect("settings serialization cannot fail");
170    write_if_absent(&project_root.join(".aether/settings.json"), &json)?;
171
172    Ok(())
173}
174
175pub fn add_agent(settings_path: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
176    let content = read_to_string(settings_path).map_err(CliError::IoError)?;
177    let slug_dir = settings_path.parent().unwrap().join("agents").join(draft.slug());
178    create_dir_all(&slug_dir).map_err(CliError::IoError)?;
179
180    let filename = format!("{}.md", draft.slug().to_uppercase());
181    write(slug_dir.join(filename), &draft.system_md_content).map_err(CliError::IoError)?;
182
183    if !draft.selected_mcp_servers.is_empty() {
184        write(slug_dir.join("mcp.json"), draft.to_mcp_json()).map_err(CliError::IoError)?;
185    }
186
187    let config = draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&content));
188    let json = serde_json::to_string_pretty(&config).expect("settings serialization cannot fail");
189    write(settings_path, json).map_err(CliError::IoError)?;
190
191    Ok(())
192}
193
194fn write_if_absent(path: &Path, content: &str) -> Result<(), CliError> {
195    if path.exists() {
196        return Ok(());
197    }
198    if let Some(parent) = path.parent() {
199        std::fs::create_dir_all(parent).map_err(CliError::IoError)?;
200    }
201    std::fs::write(path, content).map_err(CliError::IoError)?;
202    Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use llm::ReasoningEffort;
209    use mcp_utils::client::config::RawMcpConfig;
210
211    fn has_prompt(agent: &AgentConfig, path: &str) -> bool {
212        agent.prompts.iter().any(|prompt| prompt.path() == Some(path))
213    }
214
215    fn has_mcp(agent: &AgentConfig, path: &str) -> bool {
216        agent.mcps.iter().any(|mcp| mcp.path() == Some(path))
217    }
218
219    fn default_draft() -> DraftAgentEntry {
220        let mut draft = DraftAgentEntry {
221            entry: AgentConfig {
222                name: "Default".to_string(),
223                description: "Default coding agent".to_string(),
224                user_invocable: true,
225                agent_invocable: true,
226                model: "anthropic:claude-sonnet-4-5".to_string(),
227                prompts: vec![PromptSource::file("AGENTS.md")],
228                ..AgentConfig::default()
229            },
230            system_md_content: String::new(),
231            system_md_edited: false,
232            selected_mcp_servers: vec!["coding".into(), "skills".into(), "tasks".into()],
233            workspace_mcp_configs: vec![],
234        };
235        draft.system_md_content = build_system_md(&draft);
236        draft
237    }
238
239    fn researcher_draft() -> DraftAgentEntry {
240        let mut draft = default_draft();
241        draft.entry.name = "Researcher".to_string();
242        draft.entry.description = "Research agent".to_string();
243        draft.selected_mcp_servers = vec![];
244        draft.workspace_mcp_configs = vec![];
245        draft.system_md_content = build_system_md(&draft);
246        draft
247    }
248
249    #[test]
250    fn scaffold_writes_all_files() {
251        let dir = tempfile::tempdir().unwrap();
252        scaffold(dir.path(), &default_draft()).unwrap();
253
254        assert!(dir.path().join(".aether/settings.json").exists());
255        assert!(dir.path().join(".aether/mcp.json").exists());
256        assert!(dir.path().join(".aether/DEFAULT.md").exists());
257        assert!(dir.path().join("AGENTS.md").exists());
258    }
259
260    #[test]
261    fn scaffold_settings_json_is_valid() {
262        let dir = tempfile::tempdir().unwrap();
263        scaffold(dir.path(), &default_draft()).unwrap();
264
265        let config = aether_project::AetherSettings::load_default(dir.path()).unwrap();
266        let catalog = aether_project::AgentCatalog::from_settings(dir.path(), config).unwrap();
267        assert_eq!(catalog.all().len(), 1);
268        assert_eq!(catalog.all()[0].name, "Default");
269    }
270
271    #[test]
272    fn scaffold_mcp_json_is_valid() {
273        let dir = tempfile::tempdir().unwrap();
274        scaffold(dir.path(), &default_draft()).unwrap();
275
276        let raw = RawMcpConfig::from_json_file(dir.path().join(".aether/mcp.json")).unwrap();
277        assert_eq!(raw.servers.len(), 3);
278        assert!(raw.servers.contains_key("coding"));
279        assert!(raw.servers.contains_key("skills"));
280        assert!(raw.servers.contains_key("tasks"));
281    }
282
283    #[test]
284    fn scaffold_skips_existing_files() {
285        let dir = tempfile::tempdir().unwrap();
286        let agents_path = dir.path().join("AGENTS.md");
287        std::fs::write(&agents_path, "My custom prompt").unwrap();
288
289        scaffold(dir.path(), &default_draft()).unwrap();
290
291        let content = std::fs::read_to_string(&agents_path).unwrap();
292        assert_eq!(content, "My custom prompt");
293    }
294
295    #[test]
296    fn scaffold_creates_parent_dirs() {
297        let dir = tempfile::tempdir().unwrap();
298        let nested = dir.path().join("deep/nested/project");
299        scaffold(&nested, &default_draft()).unwrap();
300
301        assert!(nested.join(".aether/settings.json").exists());
302        assert!(nested.join(".aether/mcp.json").exists());
303        assert!(nested.join(".aether/DEFAULT.md").exists());
304        assert!(nested.join("AGENTS.md").exists());
305    }
306
307    #[test]
308    fn scaffold_is_idempotent() {
309        let dir = tempfile::tempdir().unwrap();
310        let draft = default_draft();
311        scaffold(dir.path(), &draft).unwrap();
312        scaffold(dir.path(), &draft).unwrap();
313        assert!(dir.path().join(".aether/settings.json").exists());
314    }
315
316    #[test]
317    fn generated_settings_reference_aether_paths() {
318        let dir = tempfile::tempdir().unwrap();
319        scaffold(dir.path(), &default_draft()).unwrap();
320
321        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
322        let config: AetherSettings = serde_json::from_str(&content).unwrap();
323        let agent = &config.agents[0];
324
325        assert_eq!(config.agents.len(), 1);
326        assert!(has_prompt(agent, ".aether/DEFAULT.md"));
327        assert!(has_prompt(agent, "AGENTS.md"));
328        assert!(has_mcp(agent, ".aether/mcp.json"));
329    }
330
331    #[test]
332    fn scaffold_without_agents_md() {
333        let dir = tempfile::tempdir().unwrap();
334        let mut draft = default_draft();
335        draft.entry.prompts = vec![];
336        scaffold(dir.path(), &draft).unwrap();
337
338        assert!(!dir.path().join("AGENTS.md").exists());
339
340        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
341        let config: AetherSettings = serde_json::from_str(&content).unwrap();
342        assert!(!has_prompt(&config.agents[0], "AGENTS.md"));
343    }
344
345    #[test]
346    fn scaffold_includes_reasoning_effort() {
347        let dir = tempfile::tempdir().unwrap();
348        let mut draft = default_draft();
349        draft.entry.reasoning_effort = Some(ReasoningEffort::High);
350        scaffold(dir.path(), &draft).unwrap();
351
352        let config = aether_project::AetherSettings::load_default(dir.path()).unwrap();
353        let catalog = aether_project::AgentCatalog::from_settings(dir.path(), config).unwrap();
354        assert_eq!(catalog.all()[0].reasoning_effort, Some(ReasoningEffort::High));
355    }
356
357    #[test]
358    fn scaffold_omits_reasoning_effort_when_none() {
359        let dir = tempfile::tempdir().unwrap();
360        scaffold(dir.path(), &default_draft()).unwrap();
361
362        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
363        assert!(!content.contains("reasoningEffort"));
364    }
365
366    #[test]
367    fn scaffold_custom_servers() {
368        let dir = tempfile::tempdir().unwrap();
369        let mut draft = default_draft();
370        draft.selected_mcp_servers = vec!["coding".into(), "lsp".into()];
371        scaffold(dir.path(), &draft).unwrap();
372
373        let raw = RawMcpConfig::from_json_file(dir.path().join(".aether/mcp.json")).unwrap();
374        assert_eq!(raw.servers.len(), 2);
375        assert!(raw.servers.contains_key("coding"));
376        assert!(raw.servers.contains_key("lsp"));
377        assert!(!raw.servers.contains_key("tasks"));
378    }
379
380    #[test]
381    fn scaffold_no_servers_no_mcp_json_ref() {
382        let dir = tempfile::tempdir().unwrap();
383        let mut draft = default_draft();
384        draft.selected_mcp_servers = vec![];
385        scaffold(dir.path(), &draft).unwrap();
386
387        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
388        let config: AetherSettings = serde_json::from_str(&content).unwrap();
389        assert!(config.agents[0].mcps.is_empty());
390    }
391
392    #[test]
393    fn add_agent_appends_to_existing_settings() {
394        let dir = tempfile::tempdir().unwrap();
395        scaffold(dir.path(), &default_draft()).unwrap();
396
397        let settings_path = dir.path().join(".aether/settings.json");
398        add_agent(&settings_path, &researcher_draft()).unwrap();
399
400        let config = aether_project::AetherSettings::load_default(dir.path()).unwrap();
401        let catalog = aether_project::AgentCatalog::from_settings(dir.path(), config).unwrap();
402        assert_eq!(catalog.all().len(), 2);
403        assert_eq!(catalog.all()[0].name, "Default");
404        assert_eq!(catalog.all()[1].name, "Researcher");
405    }
406
407    #[test]
408    fn add_agent_writes_per_agent_system_md() {
409        let dir = tempfile::tempdir().unwrap();
410        scaffold(dir.path(), &default_draft()).unwrap();
411
412        let settings_path = dir.path().join(".aether/settings.json");
413        let mut new_draft = researcher_draft();
414        new_draft.entry.prompts = vec![];
415        let expected_per_agent = new_draft.system_md_content.clone();
416        add_agent(&settings_path, &new_draft).unwrap();
417
418        let agent_md = dir.path().join(".aether/agents/researcher/RESEARCHER.md");
419        assert!(agent_md.exists());
420        assert_eq!(std::fs::read_to_string(agent_md).unwrap(), expected_per_agent);
421    }
422
423    #[test]
424    fn add_agent_writes_per_agent_mcp_json() {
425        let dir = tempfile::tempdir().unwrap();
426        scaffold(dir.path(), &default_draft()).unwrap();
427
428        let settings_path = dir.path().join(".aether/settings.json");
429        let mut new_draft = researcher_draft();
430        new_draft.entry.prompts = vec![];
431        new_draft.selected_mcp_servers = vec!["coding".into(), "lsp".into()];
432        add_agent(&settings_path, &new_draft).unwrap();
433
434        let agent_mcp = dir.path().join(".aether/agents/researcher/mcp.json");
435        assert!(agent_mcp.exists());
436
437        let raw = RawMcpConfig::from_json_file(&agent_mcp).unwrap();
438        assert_eq!(raw.servers.len(), 2);
439        assert!(raw.servers.contains_key("coding"));
440        assert!(raw.servers.contains_key("lsp"));
441    }
442
443    #[test]
444    fn add_agent_config_references_local_assets() {
445        let dir = tempfile::tempdir().unwrap();
446        scaffold(dir.path(), &default_draft()).unwrap();
447
448        let settings_path = dir.path().join(".aether/settings.json");
449        let mut new_draft = researcher_draft();
450        new_draft.entry.user_invocable = false;
451        new_draft.entry.prompts = vec![];
452        new_draft.selected_mcp_servers = vec!["coding".into()];
453        add_agent(&settings_path, &new_draft).unwrap();
454
455        let content = std::fs::read_to_string(&settings_path).unwrap();
456        let config: AetherSettings = serde_json::from_str(&content).unwrap();
457        let researcher = &config.agents[1];
458
459        assert_eq!(researcher.name, "Researcher");
460        assert!(!researcher.user_invocable);
461        assert!(researcher.agent_invocable);
462        assert!(has_prompt(researcher, ".aether/agents/researcher/RESEARCHER.md"));
463        assert!(has_mcp(researcher, ".aether/agents/researcher/mcp.json"));
464    }
465
466    #[test]
467    fn generated_paths_scaffold() {
468        let draft = default_draft();
469        let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
470        assert_eq!(paths.system_md, PathBuf::from(".aether/DEFAULT.md"));
471        assert_eq!(paths.mcp_json, PathBuf::from(".aether/mcp.json"));
472    }
473
474    #[test]
475    fn generated_paths_add_agent() {
476        let draft = default_draft();
477        let paths = draft.generated_paths(&NewAgentMode::AddAgentToExistingProject);
478        assert_eq!(paths.system_md, PathBuf::from(".aether/agents/default/DEFAULT.md"));
479        assert_eq!(paths.mcp_json, PathBuf::from(".aether/agents/default/mcp.json"));
480    }
481
482    #[test]
483    fn slug_from_name() {
484        let mut draft = default_draft();
485        draft.entry.name = "Codebase Explorer".to_string();
486        assert_eq!(draft.slug(), "codebase-explorer");
487    }
488
489    #[test]
490    fn build_system_md_uses_name_description_and_bash_block() {
491        let mut draft = default_draft();
492        draft.entry.name = "Researcher".to_string();
493        draft.entry.description = "Digs through the codebase".to_string();
494        let body = build_system_md(&draft);
495        assert!(body.starts_with("# Researcher\n"));
496        assert!(body.contains("Digs through the codebase"));
497        assert!(body.contains("## System Env"));
498        assert!(body.contains("Working directory: !`pwd`\\"));
499        assert!(body.contains("Platform: !`uname -s`\\"));
500        assert!(body.contains("Today's date: !`date +%Y-%m-%d`\\"));
501        assert!(body.contains("Git branch: !`git rev-parse --abbrev-ref HEAD`"));
502    }
503
504    #[test]
505    fn build_settings_scaffold_emits_all_selected_prompts() {
506        let mut draft = default_draft();
507        draft.entry.prompts = vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md")];
508        let config = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
509        let agent = &config.agents[0];
510
511        assert!(has_prompt(agent, ".aether/DEFAULT.md"));
512        assert!(has_prompt(agent, "AGENTS.md"));
513        assert!(has_prompt(agent, "CLAUDE.md"));
514    }
515
516    #[test]
517    fn build_settings_add_agent_skips_shared_prompts() {
518        let existing = serde_json::to_string_pretty(&AetherSettings {
519            agent: Some("Default".to_string()),
520            prompts: vec![PromptSource::file("AGENTS.md")],
521            agents: vec![AgentConfig {
522                prompts: vec![],
523                ..default_draft().to_agent_config(&NewAgentMode::ScaffoldProject, &[])
524            }],
525            ..AetherSettings::default()
526        })
527        .unwrap();
528
529        let mut new_draft = researcher_draft();
530        new_draft.entry.prompts = vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md")];
531        let config = new_draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&existing));
532
533        let researcher = &config.agents[1];
534        assert_eq!(researcher.name, "Researcher");
535        assert!(!has_prompt(researcher, "AGENTS.md"));
536        assert!(has_prompt(researcher, "CLAUDE.md"));
537    }
538
539    #[test]
540    fn scaffold_writes_agents_md_when_selected() {
541        let dir = tempfile::tempdir().unwrap();
542        let mut draft = default_draft();
543        draft.entry.prompts = vec![PromptSource::file("AGENTS.md")];
544        scaffold(dir.path(), &draft).unwrap();
545        assert!(dir.path().join("AGENTS.md").exists());
546    }
547
548    #[test]
549    fn scaffold_includes_workspace_mcp_configs() {
550        let dir = tempfile::tempdir().unwrap();
551        std::fs::write(dir.path().join("mcp.json"), r#"{"servers":{}}"#).unwrap();
552
553        let mut draft = default_draft();
554        draft.workspace_mcp_configs = vec!["mcp.json".to_string()];
555        scaffold(dir.path(), &draft).unwrap();
556
557        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
558        let config: AetherSettings = serde_json::from_str(&content).unwrap();
559
560        assert!(has_mcp(&config.agents[0], "mcp.json"));
561    }
562
563    #[test]
564    fn add_agent_includes_workspace_mcp_configs() {
565        let dir = tempfile::tempdir().unwrap();
566        scaffold(dir.path(), &default_draft()).unwrap();
567
568        let settings_path = dir.path().join(".aether/settings.json");
569        let mut new_draft = researcher_draft();
570        new_draft.selected_mcp_servers = vec!["coding".into()];
571        new_draft.workspace_mcp_configs = vec![".mcp.json".to_string()];
572        add_agent(&settings_path, &new_draft).unwrap();
573
574        let content = std::fs::read_to_string(&settings_path).unwrap();
575        let config: AetherSettings = serde_json::from_str(&content).unwrap();
576        let researcher = &config.agents[1];
577
578        assert!(has_mcp(researcher, ".mcp.json"));
579    }
580
581    #[test]
582    fn scaffold_never_writes_claude_or_gemini_md() {
583        let dir = tempfile::tempdir().unwrap();
584        let mut draft = default_draft();
585        draft.entry.prompts =
586            vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md"), PromptSource::file("GEMINI.md")];
587        scaffold(dir.path(), &draft).unwrap();
588
589        assert!(dir.path().join("AGENTS.md").exists());
590        assert!(!dir.path().join("CLAUDE.md").exists());
591        assert!(!dir.path().join("GEMINI.md").exists());
592    }
593}