mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add support for .agents/skills directory (#11181)
* feat: add support for .agents/skills directory This change adds support for discovering skills from the .agents/skills directory, following the Agent Skills convention for sharing skills across different AI coding tools. Priority order (later entries override earlier ones): 1. Global ~/.agents/skills (shared across AI coding tools, lowest priority) 2. Project .agents/skills 3. Global ~/.roo/skills (Roo-specific) 4. Project .roo/skills (highest priority) Changes: - Add getGlobalAgentsDirectory() and getProjectAgentsDirectoryForCwd() functions to roo-config - Update SkillsManager.getSkillsDirectories() to include .agents/skills - Update SkillsManager.setupFileWatchers() to watch .agents/skills - Add tests for new functionality * fix: clarify skill priority comment to match actual behavior * fix: clarify skill priority comment to explain Map.set replacement mechanism --------- Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
590fef711c
commit
a266834ee2
4 changed files with 333 additions and 11 deletions
|
|
@ -28,7 +28,9 @@ vi.mock("../../search/file-search", () => ({
|
|||
|
||||
import {
|
||||
getGlobalRooDirectory,
|
||||
getGlobalAgentsDirectory,
|
||||
getProjectRooDirectoryForCwd,
|
||||
getProjectAgentsDirectoryForCwd,
|
||||
directoryExists,
|
||||
fileExists,
|
||||
readFileIfExists,
|
||||
|
|
@ -70,6 +72,27 @@ describe("RooConfigService", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("getGlobalAgentsDirectory", () => {
|
||||
it("should return correct path for global .agents directory", () => {
|
||||
const result = getGlobalAgentsDirectory()
|
||||
expect(result).toBe(path.join("/mock/home", ".agents"))
|
||||
})
|
||||
|
||||
it("should handle different home directories", () => {
|
||||
mockHomedir.mockReturnValue("/different/home")
|
||||
const result = getGlobalAgentsDirectory()
|
||||
expect(result).toBe(path.join("/different/home", ".agents"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("getProjectAgentsDirectoryForCwd", () => {
|
||||
it("should return correct path for given cwd", () => {
|
||||
const cwd = "/custom/project/path"
|
||||
const result = getProjectAgentsDirectoryForCwd(cwd)
|
||||
expect(result).toBe(path.join(cwd, ".agents"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("directoryExists", () => {
|
||||
it("should return true for existing directory", async () => {
|
||||
mockStat.mockResolvedValue({ isDirectory: () => true } as any)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,50 @@ export function getGlobalRooDirectory(): string {
|
|||
return path.join(homeDir, ".roo")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global .agents directory path based on the current platform.
|
||||
* This is a shared directory for agent skills across different AI coding tools.
|
||||
*
|
||||
* @returns The absolute path to the global .agents directory
|
||||
*
|
||||
* @example Platform-specific paths:
|
||||
* ```
|
||||
* // macOS/Linux: ~/.agents/
|
||||
* // Example: /Users/john/.agents
|
||||
*
|
||||
* // Windows: %USERPROFILE%\.agents\
|
||||
* // Example: C:\Users\john\.agents
|
||||
* ```
|
||||
*
|
||||
* @example Usage:
|
||||
* ```typescript
|
||||
* const globalAgentsDir = getGlobalAgentsDirectory()
|
||||
* // Returns: "/Users/john/.agents" (on macOS/Linux)
|
||||
* // Returns: "C:\\Users\\john\\.agents" (on Windows)
|
||||
* ```
|
||||
*/
|
||||
export function getGlobalAgentsDirectory(): string {
|
||||
const homeDir = os.homedir()
|
||||
return path.join(homeDir, ".agents")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project-local .agents directory path for a given cwd.
|
||||
* This is a shared directory for agent skills across different AI coding tools.
|
||||
*
|
||||
* @param cwd - Current working directory (project path)
|
||||
* @returns The absolute path to the project-local .agents directory
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projectAgentsDir = getProjectAgentsDirectoryForCwd('/Users/john/my-project')
|
||||
* // Returns: "/Users/john/my-project/.agents"
|
||||
* ```
|
||||
*/
|
||||
export function getProjectAgentsDirectoryForCwd(cwd: string): string {
|
||||
return path.join(cwd, ".agents")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project-local .roo directory path for a given cwd
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as vscode from "vscode"
|
|||
import matter from "gray-matter"
|
||||
|
||||
import type { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { getGlobalRooDirectory } from "../roo-config"
|
||||
import { getGlobalRooDirectory, getGlobalAgentsDirectory, getProjectAgentsDirectoryForCwd } from "../roo-config"
|
||||
import { directoryExists, fileExists } from "../roo-config"
|
||||
import { SkillMetadata, SkillContent } from "../../shared/skills"
|
||||
import { modes, getAllModes } from "../../shared/modes"
|
||||
|
|
@ -590,19 +590,44 @@ Add your skill instructions here.
|
|||
> {
|
||||
const dirs: Array<{ dir: string; source: "global" | "project"; mode?: string }> = []
|
||||
const globalRooDir = getGlobalRooDirectory()
|
||||
const globalAgentsDir = getGlobalAgentsDirectory()
|
||||
const provider = this.providerRef.deref()
|
||||
const projectRooDir = provider?.cwd ? path.join(provider.cwd, ".roo") : null
|
||||
const projectAgentsDir = provider?.cwd ? getProjectAgentsDirectoryForCwd(provider.cwd) : null
|
||||
|
||||
// Get list of modes to check for mode-specific skills
|
||||
const modesList = await this.getAvailableModes()
|
||||
|
||||
// Global directories
|
||||
// Priority rules for skills with the same name:
|
||||
// 1. Source level: project > global > built-in (handled by shouldOverrideSkill in getSkillsForMode)
|
||||
// 2. Within the same source level: later-processed directories override earlier ones
|
||||
// (via Map.set replacement during discovery - same source+mode+name key gets replaced)
|
||||
//
|
||||
// Processing order (later directories override earlier ones at the same source level):
|
||||
// - Global: .agents/skills first, then .roo/skills (so .roo wins)
|
||||
// - Project: .agents/skills first, then .roo/skills (so .roo wins)
|
||||
|
||||
// Global .agents directories (lowest priority - shared across agents)
|
||||
dirs.push({ dir: path.join(globalAgentsDir, "skills"), source: "global" })
|
||||
for (const mode of modesList) {
|
||||
dirs.push({ dir: path.join(globalAgentsDir, `skills-${mode}`), source: "global", mode })
|
||||
}
|
||||
|
||||
// Project .agents directories
|
||||
if (projectAgentsDir) {
|
||||
dirs.push({ dir: path.join(projectAgentsDir, "skills"), source: "project" })
|
||||
for (const mode of modesList) {
|
||||
dirs.push({ dir: path.join(projectAgentsDir, `skills-${mode}`), source: "project", mode })
|
||||
}
|
||||
}
|
||||
|
||||
// Global .roo directories (Roo-specific, higher priority than .agents)
|
||||
dirs.push({ dir: path.join(globalRooDir, "skills"), source: "global" })
|
||||
for (const mode of modesList) {
|
||||
dirs.push({ dir: path.join(globalRooDir, `skills-${mode}`), source: "global", mode })
|
||||
}
|
||||
|
||||
// Project directories
|
||||
// Project .roo directories (highest priority)
|
||||
if (projectRooDir) {
|
||||
dirs.push({ dir: path.join(projectRooDir, "skills"), source: "project" })
|
||||
for (const mode of modesList) {
|
||||
|
|
@ -647,20 +672,32 @@ Add your skill instructions here.
|
|||
if (!provider?.cwd) return
|
||||
|
||||
// Watch for changes in skills directories
|
||||
const globalSkillsDir = path.join(getGlobalRooDirectory(), "skills")
|
||||
const projectSkillsDir = path.join(provider.cwd, ".roo", "skills")
|
||||
const globalRooDir = getGlobalRooDirectory()
|
||||
const globalAgentsDir = getGlobalAgentsDirectory()
|
||||
const projectRooDir = path.join(provider.cwd, ".roo")
|
||||
const projectAgentsDir = getProjectAgentsDirectoryForCwd(provider.cwd)
|
||||
|
||||
// Watch global skills directory
|
||||
this.watchDirectory(globalSkillsDir)
|
||||
// Watch global .roo skills directory
|
||||
this.watchDirectory(path.join(globalRooDir, "skills"))
|
||||
|
||||
// Watch project skills directory
|
||||
this.watchDirectory(projectSkillsDir)
|
||||
// Watch global .agents skills directory
|
||||
this.watchDirectory(path.join(globalAgentsDir, "skills"))
|
||||
|
||||
// Watch project .roo skills directory
|
||||
this.watchDirectory(path.join(projectRooDir, "skills"))
|
||||
|
||||
// Watch project .agents skills directory
|
||||
this.watchDirectory(path.join(projectAgentsDir, "skills"))
|
||||
|
||||
// Watch mode-specific directories for all available modes
|
||||
const modesList = await this.getAvailableModes()
|
||||
for (const mode of modesList) {
|
||||
this.watchDirectory(path.join(getGlobalRooDirectory(), `skills-${mode}`))
|
||||
this.watchDirectory(path.join(provider.cwd, ".roo", `skills-${mode}`))
|
||||
// .roo mode-specific
|
||||
this.watchDirectory(path.join(globalRooDir, `skills-${mode}`))
|
||||
this.watchDirectory(path.join(projectRooDir, `skills-${mode}`))
|
||||
// .agents mode-specific
|
||||
this.watchDirectory(path.join(globalAgentsDir, `skills-${mode}`))
|
||||
this.watchDirectory(path.join(projectAgentsDir, `skills-${mode}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,10 +82,13 @@ vi.mock("vscode", () => ({
|
|||
|
||||
// Global roo directory - computed once
|
||||
const GLOBAL_ROO_DIR = p(HOME_DIR, ".roo")
|
||||
const GLOBAL_AGENTS_DIR = p(HOME_DIR, ".agents")
|
||||
|
||||
// Mock roo-config
|
||||
vi.mock("../../roo-config", () => ({
|
||||
getGlobalRooDirectory: () => GLOBAL_ROO_DIR,
|
||||
getGlobalAgentsDirectory: () => GLOBAL_AGENTS_DIR,
|
||||
getProjectAgentsDirectoryForCwd: (cwd: string) => p(cwd, ".agents"),
|
||||
directoryExists: mockDirectoryExists,
|
||||
fileExists: mockFileExists,
|
||||
}))
|
||||
|
|
@ -127,6 +130,11 @@ describe("SkillsManager", () => {
|
|||
const globalSkillsArchitectDir = p(GLOBAL_ROO_DIR, "skills-architect")
|
||||
const projectRooDir = p(PROJECT_DIR, ".roo")
|
||||
const projectSkillsDir = p(projectRooDir, "skills")
|
||||
// .agents directory paths
|
||||
const globalAgentsSkillsDir = p(GLOBAL_AGENTS_DIR, "skills")
|
||||
const globalAgentsSkillsCodeDir = p(GLOBAL_AGENTS_DIR, "skills-code")
|
||||
const projectAgentsDir = p(PROJECT_DIR, ".agents")
|
||||
const projectAgentsSkillsDir = p(projectAgentsDir, "skills")
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
|
@ -615,6 +623,216 @@ Instructions here...`
|
|||
expect(skills[0].name).toBe("my-alias")
|
||||
expect(skills[0].source).toBe("global")
|
||||
})
|
||||
|
||||
it("should discover skills from global .agents directory", async () => {
|
||||
const agentSkillDir = p(globalAgentsSkillsDir, "agent-skill")
|
||||
const agentSkillMd = p(agentSkillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalAgentsSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalAgentsSkillsDir) {
|
||||
return ["agent-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === agentSkillDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
return file === agentSkillMd
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (file: string) => {
|
||||
if (file === agentSkillMd) {
|
||||
return `---
|
||||
name: agent-skill
|
||||
description: A skill from .agents directory shared across AI coding tools
|
||||
---
|
||||
|
||||
# Agent Skill
|
||||
|
||||
Instructions here...`
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
const skills = skillsManager.getAllSkills()
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0].name).toBe("agent-skill")
|
||||
expect(skills[0].description).toBe("A skill from .agents directory shared across AI coding tools")
|
||||
expect(skills[0].source).toBe("global")
|
||||
})
|
||||
|
||||
it("should discover skills from project .agents directory", async () => {
|
||||
const projectAgentSkillDir = p(projectAgentsSkillsDir, "project-agent-skill")
|
||||
const projectAgentSkillMd = p(projectAgentSkillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === projectAgentsSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === projectAgentsSkillsDir) {
|
||||
return ["project-agent-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === projectAgentSkillDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
return file === projectAgentSkillMd
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (file: string) => {
|
||||
if (file === projectAgentSkillMd) {
|
||||
return `---
|
||||
name: project-agent-skill
|
||||
description: A project-level skill from .agents directory
|
||||
---
|
||||
|
||||
# Project Agent Skill
|
||||
|
||||
Instructions here...`
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
const skills = skillsManager.getAllSkills()
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0].name).toBe("project-agent-skill")
|
||||
expect(skills[0].source).toBe("project")
|
||||
})
|
||||
|
||||
it("should prioritize .roo skills over .agents skills with same name", async () => {
|
||||
const agentSkillDir = p(globalAgentsSkillsDir, "common-skill")
|
||||
const agentSkillMd = p(agentSkillDir, "SKILL.md")
|
||||
const rooSkillDir = p(globalSkillsDir, "common-skill")
|
||||
const rooSkillMd = p(rooSkillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalAgentsSkillsDir || dir === globalSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalAgentsSkillsDir || dir === globalSkillsDir) {
|
||||
return ["common-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === agentSkillDir || pathArg === rooSkillDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
return file === agentSkillMd || file === rooSkillMd
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (file: string) => {
|
||||
if (file === agentSkillMd) {
|
||||
return `---
|
||||
name: common-skill
|
||||
description: Agent version (should be overridden)
|
||||
---
|
||||
|
||||
# Agent Common Skill`
|
||||
}
|
||||
if (file === rooSkillMd) {
|
||||
return `---
|
||||
name: common-skill
|
||||
description: Roo version (should take priority)
|
||||
---
|
||||
|
||||
# Roo Common Skill`
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
const skills = skillsManager.getSkillsForMode("code")
|
||||
const commonSkill = skills.find((s) => s.name === "common-skill")
|
||||
expect(commonSkill).toBeDefined()
|
||||
// .roo should override .agents
|
||||
expect(commonSkill?.description).toBe("Roo version (should take priority)")
|
||||
})
|
||||
|
||||
it("should discover mode-specific skills from .agents directory", async () => {
|
||||
const agentCodeSkillDir = p(globalAgentsSkillsCodeDir, "agent-code-skill")
|
||||
const agentCodeSkillMd = p(agentCodeSkillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalAgentsSkillsCodeDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalAgentsSkillsCodeDir) {
|
||||
return ["agent-code-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === agentCodeSkillDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
return file === agentCodeSkillMd
|
||||
})
|
||||
|
||||
mockReadFile.mockImplementation(async (file: string) => {
|
||||
if (file === agentCodeSkillMd) {
|
||||
return `---
|
||||
name: agent-code-skill
|
||||
description: A code mode skill from .agents directory
|
||||
---
|
||||
|
||||
# Agent Code Skill
|
||||
|
||||
Instructions here...`
|
||||
}
|
||||
throw new Error("File not found")
|
||||
})
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
const skills = skillsManager.getAllSkills()
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0].name).toBe("agent-code-skill")
|
||||
expect(skills[0].mode).toBe("code")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSkillsForMode", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue