diff --git a/src/resources/extensions/gsd/preferences-skills.ts b/src/resources/extensions/gsd/preferences-skills.ts index 1ad5a6d39..d930ba0b4 100644 --- a/src/resources/extensions/gsd/preferences-skills.ts +++ b/src/resources/extensions/gsd/preferences-skills.ts @@ -24,13 +24,18 @@ export type { GSDSkillRule, SkillDiscoveryMode, SkillResolution, SkillResolution /** * Known skill directories, in priority order. - * Global skills (~/.agents/skills/) take precedence over project skills. + * Searches both the skills.sh ecosystem directory (~/.agents/skills/) and + * Claude Code's official directory (~/.claude/skills/). Project-level + * directories for both conventions are included as well. * Legacy ~/.gsd/agent/skills/ is included as a fallback for pre-migration installs. */ export function getSkillSearchDirs(cwd: string): Array<{ dir: string; method: SkillResolution["method"] }> { const dirs: Array<{ dir: string; method: SkillResolution["method"] }> = [ { dir: join(homedir(), ".agents", "skills"), method: "user-skill" }, { dir: join(cwd, ".agents", "skills"), method: "project-skill" }, + // Claude Code official skill directories + { dir: join(homedir(), ".claude", "skills"), method: "user-skill" }, + { dir: join(cwd, ".claude", "skills"), method: "project-skill" }, ]; // Legacy fallback — read skills from old GSD directory only if migration hasn't completed const legacyDir = join(homedir(), ".gsd", "agent", "skills"); diff --git a/src/resources/extensions/gsd/skill-catalog.ts b/src/resources/extensions/gsd/skill-catalog.ts index 8f1c5d760..7a061b067 100644 --- a/src/resources/extensions/gsd/skill-catalog.ts +++ b/src/resources/extensions/gsd/skill-catalog.ts @@ -935,13 +935,16 @@ export async function installPacksBatched( /** * Check if any skills from a pack are already installed. + * Searches both the skills.sh ecosystem directory and Claude Code's official directory. */ export function isPackInstalled(pack: SkillPack): boolean { - const skillsDir = join(homedir(), ".agents", "skills"); - if (!existsSync(skillsDir)) return false; + const skillsDirs = [ + join(homedir(), ".agents", "skills"), + join(homedir(), ".claude", "skills"), + ]; return pack.skills.every((name) => - existsSync(join(skillsDir, name, "SKILL.md")), + skillsDirs.some((dir) => existsSync(join(dir, name, "SKILL.md"))), ); } diff --git a/src/resources/extensions/gsd/skill-discovery.ts b/src/resources/extensions/gsd/skill-discovery.ts index e8c224ea4..459236635 100644 --- a/src/resources/extensions/gsd/skill-discovery.ts +++ b/src/resources/extensions/gsd/skill-discovery.ts @@ -12,8 +12,9 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -/** Industry-standard skills.sh global skills directory */ +/** Skills directories — skills.sh ecosystem + Claude Code official */ const SKILLS_DIR = join(homedir(), ".agents", "skills"); +const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills"); export interface DiscoveredSkill { name: string; @@ -58,8 +59,9 @@ export function detectNewSkills(): DiscoveredSkill[] { for (const dir of current) { if (baselineSkills.has(dir)) continue; - const skillMdPath = join(SKILLS_DIR, dir, "SKILL.md"); - if (!existsSync(skillMdPath)) continue; + // Check both skill directories for the SKILL.md file + const skillMdPath = resolveSkillMdPath(dir); + if (!skillMdPath) continue; const meta = parseSkillFrontmatter(skillMdPath); if (meta) { @@ -97,10 +99,10 @@ ${entries} // ─── Internals ──────────────────────────────────────────────────────────────── -function listSkillDirs(): string[] { - if (!existsSync(SKILLS_DIR)) return []; +function listSkillDirsFrom(dir: string): string[] { + if (!existsSync(dir)) return []; try { - return readdirSync(SKILLS_DIR, { withFileTypes: true }) + return readdirSync(dir, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => d.name); } catch { @@ -108,6 +110,13 @@ function listSkillDirs(): string[] { } } +function listSkillDirs(): string[] { + const names = new Set(); + for (const name of listSkillDirsFrom(SKILLS_DIR)) names.add(name); + for (const name of listSkillDirsFrom(CLAUDE_SKILLS_DIR)) names.add(name); + return [...names]; +} + function parseSkillFrontmatter(path: string): { name?: string; description?: string } | null { try { const content = readFileSync(path, "utf-8"); @@ -131,6 +140,14 @@ function parseSkillFrontmatter(path: string): { name?: string; description?: str } } +function resolveSkillMdPath(skillName: string): string | null { + for (const dir of [SKILLS_DIR, CLAUDE_SKILLS_DIR]) { + const candidate = join(dir, skillName, "SKILL.md"); + if (existsSync(candidate)) return candidate; + } + return null; +} + function escapeXml(text: string): string { return text .replace(/&/g, "&") diff --git a/src/resources/extensions/gsd/skill-health.ts b/src/resources/extensions/gsd/skill-health.ts index 75217a5b6..6caca9464 100644 --- a/src/resources/extensions/gsd/skill-health.ts +++ b/src/resources/extensions/gsd/skill-health.ts @@ -207,9 +207,13 @@ export function formatSkillDetail(basePath: string, skillName: string): string { lines.push(` ${date} ${u.id.padEnd(20)} ${formatTokenCount(u.tokens.total).padStart(8)} tokens ${formatCost(u.cost)}`); } - // Check for SKILL.md existence - const skillPath = join(homedir(), ".agents", "skills", skillName, "SKILL.md"); - if (existsSync(skillPath)) { + // Check for SKILL.md existence — search both ecosystem and Claude Code directories + const candidatePaths = [ + join(homedir(), ".agents", "skills", skillName, "SKILL.md"), + join(homedir(), ".claude", "skills", skillName, "SKILL.md"), + ]; + const skillPath = candidatePaths.find(p => existsSync(p)); + if (skillPath) { const stat = statSync(skillPath); lines.push(""); lines.push(`SKILL.md: ${skillPath}`); diff --git a/src/resources/extensions/gsd/skill-telemetry.ts b/src/resources/extensions/gsd/skill-telemetry.ts index f1bddfd21..e5ec9c82c 100644 --- a/src/resources/extensions/gsd/skill-telemetry.ts +++ b/src/resources/extensions/gsd/skill-telemetry.ts @@ -31,12 +31,14 @@ const activelyLoadedSkills = new Set(); */ export function captureAvailableSkills(): void { const skillsDir = join(homedir(), ".agents", "skills"); + const claudeSkillsDir = join(homedir(), ".claude", "skills"); const legacyDir = join(homedir(), ".gsd", "agent", "skills"); const names = listSkillNames(skillsDir); + const claudeNames = listSkillNames(claudeSkillsDir); // Include skills still in the legacy directory only if migration hasn't completed const legacyMigrated = existsSync(join(legacyDir, ".migrated-to-agents")); const legacyNames = legacyMigrated ? [] : listSkillNames(legacyDir); - const all = new Set([...names, ...legacyNames]); + const all = new Set([...names, ...claudeNames, ...legacyNames]); availableSkills = [...all]; activelyLoadedSkills.clear(); } @@ -106,10 +108,11 @@ export function detectStaleSkills( // Check all installed skills, not just those with usage data const skillsDir = join(homedir(), ".agents", "skills"); + const claudeSkillsDir = join(homedir(), ".claude", "skills"); const legacyDir = join(homedir(), ".gsd", "agent", "skills"); const legacyMigrated = existsSync(join(legacyDir, ".migrated-to-agents")); const legacyNames = legacyMigrated ? [] : listSkillNames(legacyDir); - const installedSet = new Set([...listSkillNames(skillsDir), ...legacyNames]); + const installedSet = new Set([...listSkillNames(skillsDir), ...listSkillNames(claudeSkillsDir), ...legacyNames]); const installed = [...installedSet]; for (const skill of installed) { diff --git a/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts b/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts new file mode 100644 index 000000000..90e6aa5be --- /dev/null +++ b/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts @@ -0,0 +1,51 @@ +/** + * Tests for Claude Code skill directory support in getSkillSearchDirs(). + * + * Verifies that ~/.claude/skills/ and .claude/skills/ are included in + * the skill search path alongside ~/.agents/skills/ and .agents/skills/. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { getSkillSearchDirs } from "../preferences-skills.ts"; + +describe("getSkillSearchDirs — Claude Code directory support", () => { + const cwd = "/tmp/test-project"; + + test("includes ~/.agents/skills/ as user-skill", () => { + const dirs = getSkillSearchDirs(cwd); + const agents = dirs.find((d) => d.dir === join(homedir(), ".agents", "skills")); + assert.ok(agents, "should include ~/.agents/skills/"); + assert.equal(agents!.method, "user-skill"); + }); + + test("includes .agents/skills/ as project-skill", () => { + const dirs = getSkillSearchDirs(cwd); + const projectAgents = dirs.find((d) => d.dir === join(cwd, ".agents", "skills")); + assert.ok(projectAgents, "should include .agents/skills/"); + assert.equal(projectAgents!.method, "project-skill"); + }); + + test("includes ~/.claude/skills/ as user-skill", () => { + const dirs = getSkillSearchDirs(cwd); + const claude = dirs.find((d) => d.dir === join(homedir(), ".claude", "skills")); + assert.ok(claude, "should include ~/.claude/skills/"); + assert.equal(claude!.method, "user-skill"); + }); + + test("includes .claude/skills/ as project-skill", () => { + const dirs = getSkillSearchDirs(cwd); + const projectClaude = dirs.find((d) => d.dir === join(cwd, ".claude", "skills")); + assert.ok(projectClaude, "should include .claude/skills/"); + assert.equal(projectClaude!.method, "project-skill"); + }); + + test("~/.agents/skills/ appears before ~/.claude/skills/ (priority order)", () => { + const dirs = getSkillSearchDirs(cwd); + const agentsIdx = dirs.findIndex((d) => d.dir === join(homedir(), ".agents", "skills")); + const claudeIdx = dirs.findIndex((d) => d.dir === join(homedir(), ".claude", "skills")); + assert.ok(agentsIdx < claudeIdx, "~/.agents/skills/ should have higher priority than ~/.claude/skills/"); + }); +});