diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 9ec4f73b6d0b..de01e5960c77 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -45,6 +45,7 @@ pub struct Source { pub description: String, pub path: PathBuf, pub content: String, + pub supporting_files: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -174,6 +175,7 @@ fn parse_skill_content(content: &str, path: PathBuf) -> Option { description: metadata.description, path, content: body, + supporting_files: Vec::new(), }) } @@ -195,9 +197,36 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option { description, path, content: body, + supporting_files: Vec::new(), }) } +/// Collect all files in a skill directory (excluding SKILL.md itself), +/// recursing one level into subdirectories. +fn find_supporting_files(directory: &Path, skill_file: &Path) -> Vec { + let mut files = Vec::new(); + let entries = match std::fs::read_dir(directory) { + Ok(e) => e, + Err(_) => return files, + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && path != skill_file { + files.push(path); + } else if path.is_dir() { + if let Ok(sub_entries) = std::fs::read_dir(&path) { + for sub_entry in sub_entries.flatten() { + let sub_path = sub_entry.path(); + if sub_path.is_file() { + files.push(sub_path); + } + } + } + } + } + files +} + fn round_duration(d: Duration) -> String { let secs = d.as_secs(); if secs < 60 { @@ -614,6 +643,7 @@ impl SummonClient { description, path: PathBuf::from(&sr.path), content: String::new(), + supporting_files: Vec::new(), }); } } @@ -684,6 +714,7 @@ impl SummonClient { description: recipe.description.clone(), path: path.clone(), content: recipe.instructions.clone().unwrap_or_default(), + supporting_files: Vec::new(), }); } Err(e) => { @@ -723,8 +754,9 @@ impl SummonClient { } }; - if let Some(source) = parse_skill_content(&content, skill_file) { + if let Some(mut source) = parse_skill_content(&content, skill_dir.clone()) { if !seen.contains(&source.name) { + source.supporting_files = find_supporting_files(&skill_dir, &skill_file); seen.insert(source.name.clone()); sources.push(source); } @@ -1016,11 +1048,28 @@ impl SummonClient { Some(source) => { let content = source.to_load_text(); - let output = format!( - "# Loaded: {} ({})\n\n{}\n\n---\nThis knowledge is now available in your context.", + let mut output = format!( + "# Loaded: {} ({})\n\n{}\n", source.name, source.kind, content ); + if !source.supporting_files.is_empty() { + output.push_str(&format!( + "\n## Supporting Files\n\nSkill directory: {}\n\nThe following supporting files are available:\n", + source.path.display() + )); + for file in &source.supporting_files { + if let Ok(relative) = file.strip_prefix(&source.path) { + output.push_str(&format!("- {}\n", relative.display())); + } + } + output.push_str( + "\nUse the file tools to read these files or run scripts as directed.\n", + ); + } + + output.push_str("\n---\nThis knowledge is now available in your context."); + Ok(vec![Content::text(output)]) } None => { @@ -1837,6 +1886,37 @@ You review code."#; assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill)); } + #[tokio::test] + async fn test_skill_supporting_files_discovered() { + let temp_dir = TempDir::new().unwrap(); + + let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); + fs::create_dir_all(&skill_dir).unwrap(); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: my-skill\ndescription: A skill with scripts\n---\nRun check_all.sh", + ) + .unwrap(); + fs::write(skill_dir.join("myscript.sh"), "#!/bin/bash\necho ok").unwrap(); + fs::create_dir(skill_dir.join("templates")).unwrap(); + fs::write(skill_dir.join("templates/report.txt"), "template content").unwrap(); + + let client = SummonClient::new(create_test_context()).unwrap(); + let sources = client.discover_filesystem_sources(temp_dir.path()); + + let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); + assert_eq!(skill.path, skill_dir); + assert_eq!(skill.supporting_files.len(), 2); + + let file_names: Vec = skill + .supporting_files + .iter() + .filter_map(|f| f.file_name().map(|n| n.to_string_lossy().to_string())) + .collect(); + assert!(file_names.contains(&"myscript.sh".to_string())); + assert!(file_names.contains(&"report.txt".to_string())); + } + #[tokio::test] async fn test_client_tools_and_unknown_tool() { let client = SummonClient::new(create_test_context()).unwrap();