fix(skills): discover ancestor project skill directories
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -615,5 +615,92 @@ Skill body.
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("project")
|
||||
})
|
||||
|
||||
it("#given a skill in ancestor .agents/skills/ #when discoverProjectAgentsSkills is called from child directory #then it discovers the ancestor skill", async () => {
|
||||
// given
|
||||
const skillContent = `---
|
||||
name: ancestor-agent-skill
|
||||
description: A skill from ancestor .agents/skills directory
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const projectDir = join(TEST_DIR, "project")
|
||||
const childDir = join(projectDir, "apps", "worker")
|
||||
const agentsProjectSkillsDir = join(projectDir, ".agents", "skills")
|
||||
const skillDir = join(agentsProjectSkillsDir, "ancestor-agent-skill")
|
||||
mkdirSync(childDir, { recursive: true })
|
||||
mkdirSync(skillDir, { recursive: true })
|
||||
writeFileSync(join(skillDir, "SKILL.md"), skillContent)
|
||||
|
||||
// when
|
||||
const { discoverProjectAgentsSkills } = await import("./loader")
|
||||
const skills = await discoverProjectAgentsSkills(childDir)
|
||||
const skill = skills.find((candidate) => candidate.name === "ancestor-agent-skill")
|
||||
|
||||
// then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("project")
|
||||
})
|
||||
})
|
||||
|
||||
describe("opencode project skill discovery", () => {
|
||||
it("#given a skill in ancestor .opencode/skills/ #when discoverOpencodeProjectSkills is called from child directory #then it discovers the ancestor skill", async () => {
|
||||
// given
|
||||
const skillContent = `---
|
||||
name: ancestor-opencode-skill
|
||||
description: A skill from ancestor .opencode/skills directory
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const projectDir = join(TEST_DIR, "project")
|
||||
const childDir = join(projectDir, "packages", "cli")
|
||||
const skillsDir = join(projectDir, ".opencode", "skills", "ancestor-opencode-skill")
|
||||
mkdirSync(childDir, { recursive: true })
|
||||
mkdirSync(skillsDir, { recursive: true })
|
||||
writeFileSync(join(skillsDir, "SKILL.md"), skillContent)
|
||||
|
||||
// when
|
||||
const { discoverOpencodeProjectSkills } = await import("./loader")
|
||||
const skills = await discoverOpencodeProjectSkills(childDir)
|
||||
const skill = skills.find((candidate) => candidate.name === "ancestor-opencode-skill")
|
||||
|
||||
// then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("opencode-project")
|
||||
})
|
||||
|
||||
it("#given a skill in .opencode/skill/ #when discoverOpencodeProjectSkills is called #then it discovers the singular alias directory", async () => {
|
||||
// given
|
||||
const skillContent = `---
|
||||
name: singular-opencode-skill
|
||||
description: A skill from .opencode/skill directory
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const singularSkillDir = join(
|
||||
TEST_DIR,
|
||||
".opencode",
|
||||
"skill",
|
||||
"singular-opencode-skill",
|
||||
)
|
||||
mkdirSync(singularSkillDir, { recursive: true })
|
||||
writeFileSync(join(singularSkillDir, "SKILL.md"), skillContent)
|
||||
|
||||
// when
|
||||
const { discoverOpencodeProjectSkills } = await import("./loader")
|
||||
const originalCwd = process.cwd()
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = await discoverOpencodeProjectSkills()
|
||||
const skill = skills.find((candidate) => candidate.name === "singular-opencode-skill")
|
||||
|
||||
// then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("opencode-project")
|
||||
} finally {
|
||||
process.chdir(originalCwd)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,11 @@ import { homedir } from "os"
|
||||
import { getClaudeConfigDir } from "../../shared/claude-config-dir"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
import { getOpenCodeSkillDirs } from "../../shared/opencode-command-dirs"
|
||||
import {
|
||||
findProjectAgentsSkillDirs,
|
||||
findProjectClaudeSkillDirs,
|
||||
findProjectOpencodeSkillDirs,
|
||||
} from "../../shared/project-discovery-dirs"
|
||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||
import type { LoadedSkill } from "./types"
|
||||
import { skillsToCommandDefinitionRecord } from "./skill-definition-record"
|
||||
@@ -16,9 +21,11 @@ export async function loadUserSkills(): Promise<Record<string, CommandDefinition
|
||||
}
|
||||
|
||||
export async function loadProjectSkills(directory?: string): Promise<Record<string, CommandDefinition>> {
|
||||
const projectSkillsDir = join(directory ?? process.cwd(), ".claude", "skills")
|
||||
const skills = await loadSkillsFromDir({ skillsDir: projectSkillsDir, scope: "project" })
|
||||
return skillsToCommandDefinitionRecord(skills)
|
||||
const projectSkillDirs = findProjectClaudeSkillDirs(directory ?? process.cwd())
|
||||
const allSkills = await Promise.all(
|
||||
projectSkillDirs.map((skillsDir) => loadSkillsFromDir({ skillsDir, scope: "project" })),
|
||||
)
|
||||
return skillsToCommandDefinitionRecord(deduplicateSkillsByName(allSkills.flat()))
|
||||
}
|
||||
|
||||
export async function loadOpencodeGlobalSkills(): Promise<Record<string, CommandDefinition>> {
|
||||
@@ -30,9 +37,15 @@ export async function loadOpencodeGlobalSkills(): Promise<Record<string, Command
|
||||
}
|
||||
|
||||
export async function loadOpencodeProjectSkills(directory?: string): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "skills")
|
||||
const skills = await loadSkillsFromDir({ skillsDir: opencodeProjectDir, scope: "opencode-project" })
|
||||
return skillsToCommandDefinitionRecord(skills)
|
||||
const opencodeProjectSkillDirs = findProjectOpencodeSkillDirs(
|
||||
directory ?? process.cwd(),
|
||||
)
|
||||
const allSkills = await Promise.all(
|
||||
opencodeProjectSkillDirs.map((skillsDir) =>
|
||||
loadSkillsFromDir({ skillsDir, scope: "opencode-project" }),
|
||||
),
|
||||
)
|
||||
return skillsToCommandDefinitionRecord(deduplicateSkillsByName(allSkills.flat()))
|
||||
}
|
||||
|
||||
export interface DiscoverSkillsOptions {
|
||||
@@ -104,8 +117,11 @@ export async function discoverUserClaudeSkills(): Promise<LoadedSkill[]> {
|
||||
}
|
||||
|
||||
export async function discoverProjectClaudeSkills(directory?: string): Promise<LoadedSkill[]> {
|
||||
const projectSkillsDir = join(directory ?? process.cwd(), ".claude", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: projectSkillsDir, scope: "project" })
|
||||
const projectSkillDirs = findProjectClaudeSkillDirs(directory ?? process.cwd())
|
||||
const allSkills = await Promise.all(
|
||||
projectSkillDirs.map((skillsDir) => loadSkillsFromDir({ skillsDir, scope: "project" })),
|
||||
)
|
||||
return deduplicateSkillsByName(allSkills.flat())
|
||||
}
|
||||
|
||||
export async function discoverOpencodeGlobalSkills(): Promise<LoadedSkill[]> {
|
||||
@@ -117,13 +133,23 @@ export async function discoverOpencodeGlobalSkills(): Promise<LoadedSkill[]> {
|
||||
}
|
||||
|
||||
export async function discoverOpencodeProjectSkills(directory?: string): Promise<LoadedSkill[]> {
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: opencodeProjectDir, scope: "opencode-project" })
|
||||
const opencodeProjectSkillDirs = findProjectOpencodeSkillDirs(
|
||||
directory ?? process.cwd(),
|
||||
)
|
||||
const allSkills = await Promise.all(
|
||||
opencodeProjectSkillDirs.map((skillsDir) =>
|
||||
loadSkillsFromDir({ skillsDir, scope: "opencode-project" }),
|
||||
),
|
||||
)
|
||||
return deduplicateSkillsByName(allSkills.flat())
|
||||
}
|
||||
|
||||
export async function discoverProjectAgentsSkills(directory?: string): Promise<LoadedSkill[]> {
|
||||
const agentsProjectDir = join(directory ?? process.cwd(), ".agents", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: agentsProjectDir, scope: "project" })
|
||||
const agentsProjectSkillDirs = findProjectAgentsSkillDirs(directory ?? process.cwd())
|
||||
const allSkills = await Promise.all(
|
||||
agentsProjectSkillDirs.map((skillsDir) => loadSkillsFromDir({ skillsDir, scope: "project" })),
|
||||
)
|
||||
return deduplicateSkillsByName(allSkills.flat())
|
||||
}
|
||||
|
||||
export async function discoverGlobalAgentsSkills(): Promise<LoadedSkill[]> {
|
||||
|
||||
Reference in New Issue
Block a user