Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,22 @@ public static AgentSkill loadSkillFromDirectory(
/**
* Retrieves all skill names by parsing SKILL.md metadata in each skill folder.
*
* <p>If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single
* skill directory and only that skill's name is returned. Otherwise, subdirectories are
* scanned for skills.
*
* @param baseDir The base directory containing skill folders
* @return A list of skill names sorted alphabetically
*/
public static List<String> getAllSkillNames(Path baseDir) {
// If baseDir itself is a skill directory, return only that skill
if (hasSkillFile(baseDir)) {
List<String> names = new ArrayList<>();
readSkillName(baseDir).ifPresent(names::add);
names.sort(String::compareTo);
return names;
}

List<String> skillNames = new ArrayList<>();

try (Stream<Path> subdirs = Files.list(baseDir)) {
Expand Down Expand Up @@ -160,6 +172,10 @@ public static List<AgentSkill> getAllSkills(Path baseDir, String source) {
/**
* Retrieves all skills from the base directory.
*
* <p>If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single
* skill directory and only that skill is returned. Otherwise, subdirectories are scanned for
* skills.
*
* @param baseDir The base directory containing skill folders
* @param source The source identifier for created skills
* @param includeResources when {@code false}, each skill's resource map is left empty and
Expand All @@ -168,6 +184,17 @@ public static List<AgentSkill> getAllSkills(Path baseDir, String source) {
*/
public static List<AgentSkill> getAllSkills(
Path baseDir, String source, boolean includeResources) {
// If baseDir itself is a skill directory, return only that skill
if (hasSkillFile(baseDir)) {
List<AgentSkill> skills = new ArrayList<>();
try {
skills.add(loadSkillFromDirectory(baseDir, source, includeResources));
} catch (Exception e) {
logger.warn("Failed to load skill from '{}': {}", baseDir, e.getMessage(), e);
}
return skills;
}

List<AgentSkill> skills = new ArrayList<>();

try (Stream<Path> subdirs = Files.list(baseDir)) {
Expand Down Expand Up @@ -463,6 +490,16 @@ private static Optional<Path> findSkillDirectoryByName(Path baseDir, String skil
return Optional.empty();
}

// Check if baseDir itself is the skill
if (hasSkillFile(baseDir)) {
String rootName = readSkillName(baseDir).orElse(null);
if (skillName.equals(rootName)) {
return Optional.of(baseDir);
}
// baseDir is a single skill directory; no subdirectory skills to scan
return Optional.empty();
}

try (Stream<Path> subdirs = Files.list(baseDir)) {
return subdirs.filter(Files::isDirectory)
.filter(SkillFileSystemHelper::hasSkillFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,88 @@ void shouldFilterOsHiddenFilesOnWindows() throws IOException {
"OS hidden file should be filtered out on Windows");
}

@Test
@DisplayName("Should return root skill name when baseDir itself has SKILL.md")
void testGetAllSkillNames_RootLevelSkill() throws IOException {
Path rootSkillDir = tempDir.resolve("root-skill-dir");
Files.createDirectories(rootSkillDir);
Files.writeString(
rootSkillDir.resolve("SKILL.md"),
"---\nname: root-skill\ndescription: Root Skill\n---\nRoot content",
StandardCharsets.UTF_8);
// Add a subdirectory (e.g. references/) that should be ignored
Files.createDirectories(rootSkillDir.resolve("references"));
Files.writeString(rootSkillDir.resolve("references/doc.md"), "doc");

List<String> names = SkillFileSystemHelper.getAllSkillNames(rootSkillDir);
assertEquals(1, names.size());
assertEquals("root-skill", names.get(0));
}

@Test
@DisplayName("Should return root skill when baseDir itself has SKILL.md")
void testGetAllSkills_RootLevelSkill() throws IOException {
Path rootSkillDir = tempDir.resolve("root-skill-dir2");
Files.createDirectories(rootSkillDir);
Files.writeString(
rootSkillDir.resolve("SKILL.md"),
"---\nname: root-skill2\ndescription: Root Skill 2\n---\nRoot content 2",
StandardCharsets.UTF_8);
Files.writeString(rootSkillDir.resolve("extra.txt"), "extra resource");

List<AgentSkill> skills = SkillFileSystemHelper.getAllSkills(rootSkillDir, "git-source");
assertEquals(1, skills.size());
assertEquals("root-skill2", skills.get(0).getName());
assertEquals("git-source", skills.get(0).getSource());
assertTrue(skills.get(0).getResources().containsKey("extra.txt"));
}

@Test
@DisplayName("Should load root skill by name when baseDir itself has SKILL.md")
void testLoadSkill_RootLevelSkill() throws IOException {
Path rootSkillDir = tempDir.resolve("root-skill-dir3");
Files.createDirectories(rootSkillDir);
Files.writeString(
rootSkillDir.resolve("SKILL.md"),
"---\nname: root-skill3\ndescription: Root Skill 3\n---\nRoot content 3",
StandardCharsets.UTF_8);

AgentSkill skill =
SkillFileSystemHelper.loadSkill(rootSkillDir, "root-skill3", "git-source");
assertNotNull(skill);
assertEquals("root-skill3", skill.getName());
assertEquals("Root Skill 3", skill.getDescription());
}

@Test
@DisplayName("Should find root skill via skillExists when baseDir itself has SKILL.md")
void testSkillExists_RootLevelSkill() throws IOException {
Path rootSkillDir = tempDir.resolve("root-skill-dir4");
Files.createDirectories(rootSkillDir);
Files.writeString(
rootSkillDir.resolve("SKILL.md"),
"---\nname: root-skill4\ndescription: Root Skill 4\n---\nContent",
StandardCharsets.UTF_8);

assertTrue(SkillFileSystemHelper.skillExists(rootSkillDir, "root-skill4"));
assertFalse(SkillFileSystemHelper.skillExists(rootSkillDir, "nonexistent"));
}

@Test
@DisplayName("Should throw when root skill name does not match requested name")
void testLoadSkill_RootLevelSkillNameMismatch() throws IOException {
Path rootSkillDir = tempDir.resolve("root-skill-dir5");
Files.createDirectories(rootSkillDir);
Files.writeString(
rootSkillDir.resolve("SKILL.md"),
"---\nname: actual-name\ndescription: Actual\n---\nContent",
StandardCharsets.UTF_8);

assertThrows(
IllegalArgumentException.class,
() -> SkillFileSystemHelper.loadSkill(rootSkillDir, "wrong-name", "source"));
}

private void createSampleSkill(String name, String description, String content)
throws IOException {
Path skillDir = skillsBaseDir.resolve(name);
Expand Down
Loading