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:
YeonGyu-Kim 2026-02-14 15:01:18 +09:00 committed by GitHub
commit 90463bafd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 167 additions and 29 deletions

View File

@ -28,13 +28,13 @@
"typescript": "^5.7.3", "typescript": "^5.7.3",
}, },
"optionalDependencies": { "optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.5.2", "oh-my-opencode-darwin-arm64": "3.5.3",
"oh-my-opencode-darwin-x64": "3.5.2", "oh-my-opencode-darwin-x64": "3.5.3",
"oh-my-opencode-linux-arm64": "3.5.2", "oh-my-opencode-linux-arm64": "3.5.3",
"oh-my-opencode-linux-arm64-musl": "3.5.2", "oh-my-opencode-linux-arm64-musl": "3.5.3",
"oh-my-opencode-linux-x64": "3.5.2", "oh-my-opencode-linux-x64": "3.5.3",
"oh-my-opencode-linux-x64-musl": "3.5.2", "oh-my-opencode-linux-x64-musl": "3.5.3",
"oh-my-opencode-windows-x64": "3.5.2", "oh-my-opencode-windows-x64": "3.5.3",
}, },
}, },
}, },
@ -226,19 +226,19 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oIS3lB2F9/N+3mF5wCKk6/EPVSz516XWN+mNdquSSeddw+xqMxGdhKY6K/XeYbHJzeN2Z8IOikNEJ6psR2/a8g=="], "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Dq0+PC2dyAqG7c3DUnQmdOkKbKmOsRHwoqgLCQNKN1lTRllF8zbWqp5B+LGKxSPxPqJIPS3mKt+wIR2KvkYJVw=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.2", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OAdXo4ZCCYO4kRWtnyz3tdmaGYPUB3WcXimXAxp+/sEZxAnh7n1RQkpLn6UxWX4AIAdRT9dfrOfRic6VoCYv2g=="], "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Ke45Bv/ygZm3YUSUumIyk647KZ2PFzw30tH597cOpG8MDPGbNVBCM6EKFezcukUPT+gPFVpE1IiGzEkn4JmgZA=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5XXNMFhp1VsyrGNRBoXcOyoaUeVkbrWkBRPDGZfpiq+kRXH3aaSWdR5G7Pl/TadOQv9Bl8/8YaxsuHRTFT1aXw=="], "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aP5S3DngUhFkNeqYM33Ge6zccCWLzB/O3FLXLFXy/Iws03N8xugw72pnMK6lUbIia9QQBKK7IZBoYm9C79pZ3g=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/woIpqvEI85MgJvEVnz4g5FBLeiQNK7srRsueIFPBmtTahh42HFleCDaIltOl/ndjsE5nCHacQVJHkC9W9/F3Q=="], "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UiD/hVKYZQyX4D5N5SnZT4M5Z/B2SDtJWBW4MibpYSAcPKNCEBKi/5E4hOPxAtTfFGR8tIXFmYZdQJDkVfvluw=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vTL2A+6zzGhi+m7sC8peLDq5OAp2dRR0UEb4RbZAOHtlEruF7qFEmcK3ccWxwc3+Z3G/ITfwn5VNa72ZS4pNTg=="], "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-L9kqwzElGkaQ8pgtv1ZjcHARw9LPaU4UEVjzauByTMi+/5Js/PTsNXBggxSRzZfQ8/MNBPSCiA4K10Kc0YjjvA=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-bOAA55snLsK2QB00IkQy8le0Oqh/GJ7pxEHtm1oUezlQrW/nX5SS/hJ7dPHMmOd9FoiqnqyqWZxNkLmFoG463A=="], "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Z0fVVih/b2dbNeb9DK9oca5dNYCZyPySBRtxRhDXod5d7fJNgIPrvUoEd3SNfkRGORyFB3hGBZ6nqQ6N8+8DEA=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.2", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-fnHiAPYglw3unPckmQBoCT6+VqjSWCE3S3J551mRo0ZFrxuEP2ZKyHZeFMMOtKwDepCvmKgd1W040+KmuVUXOA=="], "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-ocWPjRs2sJgN02PJnEIYtqdMVDex1YhEj1FzAU5XIicfzQbgxLh9nz1yhHZzfqGJq69QStU6ofpc5kQpfX1LMg=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],

View File

@ -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")
})
})

View File

@ -552,7 +552,7 @@ Skill body.
expect(names.length).toBe(uniqueNames.length) expect(names.length).toBe(uniqueNames.length)
} finally { } finally {
process.chdir(originalCwd) process.chdir(originalCwd)
if (originalOpenCodeConfigDir === undefined) { if (originalOpenCodeConfigDir === undefined) {
delete process.env.OPENCODE_CONFIG_DIR delete process.env.OPENCODE_CONFIG_DIR
} else { } else {
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir 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")
})
})
}) })

View File

@ -1,4 +1,5 @@
import { join } from "path" import { join } from "path"
import { homedir } from "os"
import { getClaudeConfigDir } from "../../shared/claude-config-dir" import { getClaudeConfigDir } from "../../shared/claude-config-dir"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir" import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
import type { CommandDefinition } from "../claude-code-command-loader/types" import type { CommandDefinition } from "../claude-code-command-loader/types"
@ -38,15 +39,25 @@ export interface DiscoverSkillsOptions {
} }
export async function discoverAllSkills(directory?: string): Promise<LoadedSkill[]> { export async function discoverAllSkills(directory?: string): Promise<LoadedSkill[]> {
const [opencodeProjectSkills, opencodeGlobalSkills, projectSkills, userSkills] = await Promise.all([ const [opencodeProjectSkills, opencodeGlobalSkills, projectSkills, userSkills, agentsProjectSkills, agentsGlobalSkills] =
discoverOpencodeProjectSkills(directory), await Promise.all([
discoverOpencodeGlobalSkills(), discoverOpencodeProjectSkills(directory),
discoverProjectClaudeSkills(directory), discoverOpencodeGlobalSkills(),
discoverUserClaudeSkills(), discoverProjectClaudeSkills(directory),
]) discoverUserClaudeSkills(),
discoverProjectAgentsSkills(directory),
discoverGlobalAgentsSkills(),
])
// Priority: opencode-project > opencode > project > user // Priority: opencode-project > opencode > project (.claude + .agents) > user (.claude + .agents)
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills]) return deduplicateSkillsByName([
...opencodeProjectSkills,
...opencodeGlobalSkills,
...projectSkills,
...agentsProjectSkills,
...userSkills,
...agentsGlobalSkills,
])
} }
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> { export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
@ -62,13 +73,22 @@ export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promi
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills]) return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills])
} }
const [projectSkills, userSkills] = await Promise.all([ const [projectSkills, userSkills, agentsProjectSkills, agentsGlobalSkills] = await Promise.all([
discoverProjectClaudeSkills(directory), discoverProjectClaudeSkills(directory),
discoverUserClaudeSkills(), discoverUserClaudeSkills(),
discoverProjectAgentsSkills(directory),
discoverGlobalAgentsSkills(),
]) ])
// Priority: opencode-project > opencode > project > user // Priority: opencode-project > opencode > project (.claude + .agents) > user (.claude + .agents)
return deduplicateSkillsByName([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills]) return deduplicateSkillsByName([
...opencodeProjectSkills,
...opencodeGlobalSkills,
...projectSkills,
...agentsProjectSkills,
...userSkills,
...agentsGlobalSkills,
])
} }
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> { 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") const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "skills")
return loadSkillsFromDir({ skillsDir: opencodeProjectDir, scope: "opencode-project" }) 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" })
}

View File

@ -12,6 +12,8 @@ import {
discoverProjectClaudeSkills, discoverProjectClaudeSkills,
discoverOpencodeGlobalSkills, discoverOpencodeGlobalSkills,
discoverOpencodeProjectSkills, discoverOpencodeProjectSkills,
discoverProjectAgentsSkills,
discoverGlobalAgentsSkills,
mergeSkills, mergeSkills,
} from "../features/opencode-skill-loader" } from "../features/opencode-skill-loader"
import { createBuiltinSkills } from "../features/builtin-skills" import { createBuiltinSkills } from "../features/builtin-skills"
@ -55,7 +57,7 @@ export async function createSkillContext(args: {
}) })
const includeClaudeSkills = pluginConfig.claude_code?.skills !== false const includeClaudeSkills = pluginConfig.claude_code?.skills !== false
const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills] = const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills, agentsProjectSkills, agentsGlobalSkills] =
await Promise.all([ await Promise.all([
discoverConfigSourceSkills({ discoverConfigSourceSkills({
config: pluginConfig.skills, config: pluginConfig.skills,
@ -65,15 +67,17 @@ export async function createSkillContext(args: {
discoverOpencodeGlobalSkills(), discoverOpencodeGlobalSkills(),
includeClaudeSkills ? discoverProjectClaudeSkills(directory) : Promise.resolve([]), includeClaudeSkills ? discoverProjectClaudeSkills(directory) : Promise.resolve([]),
discoverOpencodeProjectSkills(directory), discoverOpencodeProjectSkills(directory),
discoverProjectAgentsSkills(directory),
discoverGlobalAgentsSkills(),
]) ])
const mergedSkills = mergeSkills( const mergedSkills = mergeSkills(
builtinSkills, builtinSkills,
pluginConfig.skills, pluginConfig.skills,
configSourceSkills, configSourceSkills,
userSkills, [...userSkills, ...agentsGlobalSkills],
globalSkills, globalSkills,
projectSkills, [...projectSkills, ...agentsProjectSkills],
opencodeProjectSkills, opencodeProjectSkills,
{ configDir: directory }, { configDir: directory },
) )