From fedc8e79ebd51f0c2bc9f6f3be8d7c47efbb8b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=9D=E6=B2=90?= <109275489+deijing@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:14:29 +0800 Subject: [PATCH] feat: add ~/.claude/skills/ and .claude/skills/ directory scanning support (#288) --- cli/src/modules/common/skills.test.ts | 46 +++++++++++++++++++++++++++ cli/src/modules/common/skills.ts | 21 ++++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/cli/src/modules/common/skills.test.ts b/cli/src/modules/common/skills.test.ts index 2679cbc3..2b6776b4 100644 --- a/cli/src/modules/common/skills.test.ts +++ b/cli/src/modules/common/skills.test.ts @@ -50,6 +50,25 @@ describe('listSkills', () => { expect(skills.map((skill) => skill.name)).toEqual(['amis']) }) + it('lists user skills from ~/.claude/skills', async () => { + await writeSkill(join(homeDir, '.claude', 'skills', 'claude-skill'), 'claude-skill', 'Claude skill') + + const skills = await listSkills() + + expect(skills.map((skill) => skill.name)).toEqual(['claude-skill']) + }) + + it('merges user skills from ~/.agents and ~/.claude, preferring ~/.agents', async () => { + await writeSkill(join(homeDir, '.agents', 'skills', 'alpha'), 'alpha', 'Alpha from agents') + await writeSkill(join(homeDir, '.claude', 'skills', 'beta'), 'beta', 'Beta from claude') + await writeSkill(join(homeDir, '.claude', 'skills', 'alpha'), 'alpha', 'Alpha from claude') + + const skills = await listSkills() + + expect(skills.map((skill) => skill.name)).toEqual(['alpha', 'beta']) + expect(skills.find((s) => s.name === 'alpha')?.description).toBe('Alpha from agents') + }) + it('ignores legacy ~/.codex skills', async () => { await writeSkill(join(homeDir, '.agents', 'skills', 'amis'), 'amis', 'AMIS guide') await writeSkill(join(homeDir, '.codex', 'skills', 'hello-agents'), 'helloagents', 'Main skill') @@ -86,6 +105,33 @@ describe('listSkills', () => { expect(skills.map((skill) => skill.name)).toEqual(['local-skill', 'package-skill', 'root-skill']) }) + it('loads project skills from .claude/skills directories', async () => { + const repoRoot = join(sandboxDir, 'repo') + const workingDirectory = join(repoRoot, 'apps', 'web') + + await mkdir(join(repoRoot, '.git'), { recursive: true }) + await writeSkill(join(repoRoot, '.claude', 'skills', 'claude-root'), 'claude-root', 'Claude root skill') + await writeSkill(join(workingDirectory, '.claude', 'skills', 'claude-local'), 'claude-local', 'Claude local skill') + + const skills = await listSkills(workingDirectory) + + expect(skills.map((skill) => skill.name)).toEqual(['claude-local', 'claude-root']) + }) + + it('prefers .agents project skills over .claude project skills with same name', async () => { + const repoRoot = join(sandboxDir, 'repo') + const workingDirectory = join(repoRoot, 'apps', 'web') + + await mkdir(join(repoRoot, '.git'), { recursive: true }) + await writeSkill(join(workingDirectory, '.agents', 'skills', 'shared'), 'shared', 'From agents') + await writeSkill(join(workingDirectory, '.claude', 'skills', 'shared'), 'shared', 'From claude') + + const skills = await listSkills(workingDirectory) + + expect(skills).toHaveLength(1) + expect(skills[0]).toEqual({ name: 'shared', description: 'From agents' }) + }) + it('uses only cwd project skills outside a git repository', async () => { const parentDirectory = join(sandboxDir, 'workspace') const workingDirectory = join(parentDirectory, 'feature') diff --git a/cli/src/modules/common/skills.ts b/cli/src/modules/common/skills.ts index 9090987a..b10b6c1c 100644 --- a/cli/src/modules/common/skills.ts +++ b/cli/src/modules/common/skills.ts @@ -21,16 +21,23 @@ function getHomeDirectory(): string { return process.env.HOME ?? process.env.USERPROFILE ?? homedir(); } -function getUserSkillsRoot(): string { - return join(getHomeDirectory(), '.agents', 'skills'); +function getUserSkillsRoots(): string[] { + const home = getHomeDirectory(); + return [ + join(home, '.agents', 'skills'), + join(home, '.claude', 'skills'), + ]; } function getAdminSkillsRoot(): string { return join('/etc', 'codex', 'skills'); } -function getProjectSkillsRoot(directory: string): string { - return join(directory, '.agents', 'skills'); +function getProjectSkillsRoots(directory: string): string[] { + return [ + join(directory, '.agents', 'skills'), + join(directory, '.claude', 'skills'), + ]; } async function pathExists(path: string): Promise { @@ -53,12 +60,12 @@ async function listProjectSkillsRoots(workingDirectory?: string): Promise await listTopLevelSkillDirs(root))).then((dirs) => dirs.flat()), - listTopLevelSkillDirs(getUserSkillsRoot()), + Promise.all(getUserSkillsRoots().map(async (root) => await listTopLevelSkillDirs(root))).then((dirs) => dirs.flat()), listTopLevelSkillDirs(getAdminSkillsRoot()), ]);