Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/services/roo-config/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ vi.mock("../../search/file-search", () => ({

import {
getGlobalRooDirectory,
getGlobalAgentsDirectory,
getProjectRooDirectoryForCwd,
getProjectAgentsDirectoryForCwd,
directoryExists,
fileExists,
readFileIfExists,
Expand Down Expand Up @@ -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)
Expand Down
44 changes: 44 additions & 0 deletions src/services/roo-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
59 changes: 48 additions & 11 deletions src/services/skills/SkillsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 .roo skills directory
this.watchDirectory(path.join(globalRooDir, "skills"))

// Watch global .agents skills directory
this.watchDirectory(path.join(globalAgentsDir, "skills"))

// Watch global skills directory
this.watchDirectory(globalSkillsDir)
// Watch project .roo skills directory
this.watchDirectory(path.join(projectRooDir, "skills"))

// Watch project skills directory
this.watchDirectory(projectSkillsDir)
// 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}`))
}
}

Expand Down
Loading
Loading