MCPcopy Create free account
hub / github.com/Zoo-Code-Org/Zoo-Code / SkillsManager

Class SkillsManager

src/services/skills/SkillsManager.ts:21–719  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

19export type { SkillMetadata, SkillContent }
20
21export class SkillsManager {
22 private skills: Map<string, SkillMetadata> = new Map()
23 private providerRef: WeakRef<ClineProvider>
24 private disposables: vscode.Disposable[] = []
25 private isDisposed = false
26
27 constructor(provider: ClineProvider) {
28 this.providerRef = new WeakRef(provider)
29 }
30
31 async initialize(): Promise<void> {
32 await this.discoverSkills()
33 await this.setupFileWatchers()
34 }
35
36 /**
37 * Discover all skills from global and project directories.
38 * Supports both generic skills (skills/) and mode-specific skills (skills-{mode}/).
39 * Also supports symlinks:
40 * - .roo/skills can be a symlink to a directory containing skill subdirectories
41 * - .roo/skills/[dirname] can be a symlink to a skill directory
42 */
43 async discoverSkills(): Promise<void> {
44 this.skills.clear()
45 const skillsDirs = await this.getSkillsDirectories()
46
47 for (const { dir, source, mode } of skillsDirs) {
48 await this.scanSkillsDirectory(dir, source, mode)
49 }
50 }
51
52 /**
53 * Scan a skills directory for skill subdirectories.
54 * Handles two symlink cases:
55 * 1. The skills directory itself is a symlink (resolved by directoryExists using realpath)
56 * 2. Individual skill subdirectories are symlinks
57 */
58 private async scanSkillsDirectory(dirPath: string, source: "global" | "project", mode?: string): Promise<void> {
59 if (!(await directoryExists(dirPath))) {
60 return
61 }
62
63 try {
64 // Get the real path (resolves if dirPath is a symlink)
65 const realDirPath = await fs.realpath(dirPath)
66
67 // Read directory entries
68 const entries = await fs.readdir(realDirPath)
69
70 for (const entryName of entries) {
71 const entryPath = path.join(realDirPath, entryName)
72
73 // Check if this entry is a directory (follows symlinks automatically)
74 const stats = await fs.stat(entryPath).catch(() => null)
75 if (!stats?.isDirectory()) continue
76
77 // Load skill metadata - the skill name comes from the entry name (symlink name if symlinked)
78 await this.loadSkillMetadata(entryPath, source, mode, entryName)

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected