Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions crates/goose/src/agents/platform_extensions/summon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Source {
pub description: String,
pub path: PathBuf,
pub content: String,
pub supporting_files: Vec<PathBuf>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -174,6 +175,7 @@ fn parse_skill_content(content: &str, path: PathBuf) -> Option<Source> {
description: metadata.description,
path,
content: body,
supporting_files: Vec::new(),
})
}

Expand All @@ -195,9 +197,36 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
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<PathBuf> {
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 {
Expand Down Expand Up @@ -614,6 +643,7 @@ impl SummonClient {
description,
path: PathBuf::from(&sr.path),
content: String::new(),
supporting_files: Vec::new(),
});
}
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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<String> = 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();
Expand Down
Loading