Merge pull request #1834 from code-yeongyu/fix/issue-1818-agents-skills-path
fix(skill-loader): discover skills from .agents/skills/ directory
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
import { mkdirSync, writeFileSync, rmSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { tmpdir } from "os"
|
||||
|
||||
const TEST_DIR = join(tmpdir(), "agents-global-skills-test-" + Date.now())
|
||||
const TEMP_HOME = join(TEST_DIR, "home")
|
||||
|
||||
describe("discoverGlobalAgentsSkills", () => {
|
||||
beforeEach(() => {
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
mkdirSync(TEMP_HOME, { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("#given a skill in ~/.agents/skills/ #when discoverGlobalAgentsSkills is called #then it discovers the skill", async () => {
|
||||
//#given
|
||||
const skillContent = `---
|
||||
name: agent-global-skill
|
||||
description: A skill from global .agents/skills directory
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const agentsGlobalSkillsDir = join(TEMP_HOME, ".agents", "skills")
|
||||
const skillDir = join(agentsGlobalSkillsDir, "agent-global-skill")
|
||||
mkdirSync(skillDir, { recursive: true })
|
||||
writeFileSync(join(skillDir, "SKILL.md"), skillContent)
|
||||
|
||||
mock.module("os", () => ({
|
||||
homedir: () => TEMP_HOME,
|
||||
tmpdir,
|
||||
}))
|
||||
|
||||
//#when
|
||||
const { discoverGlobalAgentsSkills } = await import("./loader")
|
||||
const skills = await discoverGlobalAgentsSkills()
|
||||
const skill = skills.find(s => s.name === "agent-global-skill")
|
||||
|
||||
//#then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("user")
|
||||
expect(skill?.definition.description).toContain("A skill from global .agents/skills directory")
|
||||
})
|
||||
})
|
||||
@@ -552,7 +552,7 @@ Skill body.
|
||||
expect(names.length).toBe(uniqueNames.length)
|
||||
} finally {
|
||||
process.chdir(originalCwd)
|
||||
if (originalOpenCodeConfigDir === undefined) {
|
||||
if (originalOpenCodeConfigDir === undefined) {
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
|
||||
@@ -560,4 +560,60 @@ Skill body.
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("agents skills discovery (.agents/skills/)", () => {
|
||||
it("#given a skill in .agents/skills/ #when discoverProjectAgentsSkills is called #then it discovers the skill", async () => {
|
||||
//#given
|
||||
const skillContent = `---
|
||||
name: agent-project-skill
|
||||
description: A skill from project .agents/skills directory
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const agentsProjectSkillsDir = join(TEST_DIR, ".agents", "skills")
|
||||
const skillDir = join(agentsProjectSkillsDir, "agent-project-skill")
|
||||
mkdirSync(skillDir, { recursive: true })
|
||||
writeFileSync(join(skillDir, "SKILL.md"), skillContent)
|
||||
|
||||
//#when
|
||||
const { discoverProjectAgentsSkills } = await import("./loader")
|
||||
const originalCwd = process.cwd()
|
||||
process.chdir(TEST_DIR)
|
||||
|
||||
try {
|
||||
const skills = await discoverProjectAgentsSkills()
|
||||
const skill = skills.find(s => s.name === "agent-project-skill")
|
||||
|
||||
//#then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("project")
|
||||
expect(skill?.definition.description).toContain("A skill from project .agents/skills directory")
|
||||
} finally {
|
||||
process.chdir(originalCwd)
|
||||
}
|
||||
})
|
||||
|
||||
it("#given a skill in .agents/skills/ #when discoverProjectAgentsSkills is called with directory #then it discovers the skill", async () => {
|
||||
//#given
|
||||
const skillContent = `---
|
||||
name: agent-dir-skill
|
||||
description: A skill via explicit directory param
|
||||
---
|
||||
Skill body.
|
||||
`
|
||||
const agentsProjectSkillsDir = join(TEST_DIR, ".agents", "skills")
|
||||
const skillDir = join(agentsProjectSkillsDir, "agent-dir-skill")
|
||||
mkdirSync(skillDir, { recursive: true })
|
||||
writeFileSync(join(skillDir, "SKILL.md"), skillContent)
|
||||
|
||||
//#when
|
||||
const { discoverProjectAgentsSkills } = await import("./loader")
|
||||
const skills = await discoverProjectAgentsSkills(TEST_DIR)
|
||||
const skill = skills.find(s => s.name === "agent-dir-skill")
|
||||
|
||||
//#then
|
||||
expect(skill).toBeDefined()
|
||||
expect(skill?.scope).toBe("project")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { join } from "path"
|
||||
import { homedir } from "os"
|
||||
import { getClaudeConfigDir } from "../../shared/claude-config-dir"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||
@@ -38,15 +39,25 @@ export interface DiscoverSkillsOptions {
|
||||
}
|
||||
|
||||
export async function discoverAllSkills(directory?: string): Promise<LoadedSkill[]> {
|
||||
const [opencodeProjectSkills, opencodeGlobalSkills, projectSkills, userSkills] = await Promise.all([
|
||||
discoverOpencodeProjectSkills(directory),
|
||||
discoverOpencodeGlobalSkills(),
|
||||
discoverProjectClaudeSkills(directory),
|
||||
discoverUserClaudeSkills(),
|
||||
])
|
||||
const [opencodeProjectSkills, opencodeGlobalSkills, projectSkills, userSkills, agentsProjectSkills, agentsGlobalSkills] =
|
||||
await Promise.all([
|
||||
discoverOpencodeProjectSkills(directory),
|
||||
discoverOpencodeGlobalSkills(),
|
||||
discoverProjectClaudeSkills(directory),
|
||||
discoverUserClaudeSkills(),
|
||||
discoverProjectAgentsSkills(directory),
|
||||
discoverGlobalAgentsSkills(),
|
||||
])
|
||||
|
||||
// Priority: opencode-project > opencode > project > user
|
||||
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
|
||||
// Priority: opencode-project > opencode > project (.claude + .agents) > user (.claude + .agents)
|
||||
return deduplicateSkillsByName([
|
||||
...opencodeProjectSkills,
|
||||
...opencodeGlobalSkills,
|
||||
...projectSkills,
|
||||
...agentsProjectSkills,
|
||||
...userSkills,
|
||||
...agentsGlobalSkills,
|
||||
])
|
||||
}
|
||||
|
||||
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
||||
@@ -62,13 +73,22 @@ export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promi
|
||||
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills])
|
||||
}
|
||||
|
||||
const [projectSkills, userSkills] = await Promise.all([
|
||||
const [projectSkills, userSkills, agentsProjectSkills, agentsGlobalSkills] = await Promise.all([
|
||||
discoverProjectClaudeSkills(directory),
|
||||
discoverUserClaudeSkills(),
|
||||
discoverProjectAgentsSkills(directory),
|
||||
discoverGlobalAgentsSkills(),
|
||||
])
|
||||
|
||||
// Priority: opencode-project > opencode > project > user
|
||||
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
|
||||
// Priority: opencode-project > opencode > project (.claude + .agents) > user (.claude + .agents)
|
||||
return deduplicateSkillsByName([
|
||||
...opencodeProjectSkills,
|
||||
...opencodeGlobalSkills,
|
||||
...projectSkills,
|
||||
...agentsProjectSkills,
|
||||
...userSkills,
|
||||
...agentsGlobalSkills,
|
||||
])
|
||||
}
|
||||
|
||||
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> {
|
||||
@@ -96,3 +116,13 @@ export async function discoverOpencodeProjectSkills(directory?: string): Promise
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: opencodeProjectDir, scope: "opencode-project" })
|
||||
}
|
||||
|
||||
export async function discoverProjectAgentsSkills(directory?: string): Promise<LoadedSkill[]> {
|
||||
const agentsProjectDir = join(directory ?? process.cwd(), ".agents", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: agentsProjectDir, scope: "project" })
|
||||
}
|
||||
|
||||
export async function discoverGlobalAgentsSkills(): Promise<LoadedSkill[]> {
|
||||
const agentsGlobalDir = join(homedir(), ".agents", "skills")
|
||||
return loadSkillsFromDir({ skillsDir: agentsGlobalDir, scope: "user" })
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
discoverProjectClaudeSkills,
|
||||
discoverOpencodeGlobalSkills,
|
||||
discoverOpencodeProjectSkills,
|
||||
discoverProjectAgentsSkills,
|
||||
discoverGlobalAgentsSkills,
|
||||
mergeSkills,
|
||||
} from "../features/opencode-skill-loader"
|
||||
import { createBuiltinSkills } from "../features/builtin-skills"
|
||||
@@ -55,7 +57,7 @@ export async function createSkillContext(args: {
|
||||
})
|
||||
|
||||
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false
|
||||
const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills] =
|
||||
const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills, agentsProjectSkills, agentsGlobalSkills] =
|
||||
await Promise.all([
|
||||
discoverConfigSourceSkills({
|
||||
config: pluginConfig.skills,
|
||||
@@ -65,15 +67,17 @@ export async function createSkillContext(args: {
|
||||
discoverOpencodeGlobalSkills(),
|
||||
includeClaudeSkills ? discoverProjectClaudeSkills(directory) : Promise.resolve([]),
|
||||
discoverOpencodeProjectSkills(directory),
|
||||
discoverProjectAgentsSkills(directory),
|
||||
discoverGlobalAgentsSkills(),
|
||||
])
|
||||
|
||||
const mergedSkills = mergeSkills(
|
||||
builtinSkills,
|
||||
pluginConfig.skills,
|
||||
configSourceSkills,
|
||||
userSkills,
|
||||
[...userSkills, ...agentsGlobalSkills],
|
||||
globalSkills,
|
||||
projectSkills,
|
||||
[...projectSkills, ...agentsProjectSkills],
|
||||
opencodeProjectSkills,
|
||||
{ configDir: directory },
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user