@@ -4,112 +4,216 @@ import (
44 "context"
55 "os"
66 "path/filepath"
7+ "strings"
78
89 "github.com/safedep/vet/pkg/inventory"
910)
1011
11- const scannerName = "skills"
12+ const (
13+ scannerName = "skills"
1214
13- // agentEntry maps an agent identifier to its project-local and global skill
14- // directory paths. ProjectPath is relative to cfg.ProjectDir; GlobalPath is
15- // relative to cfg.HomeDir.
16- type agentEntry struct {
17- App string
18- ProjectPath string
19- GlobalPath string
15+ // appGlobalSkills is the App name used for skill directories that are
16+ // shared by multiple agents (e.g. .agents/skills/). Using a single name
17+ // avoids emitting the same skill once per agent that claims the path.
18+ appGlobalSkills = "Global Skills"
19+ )
20+
21+ // pathEntry is a (relative path, app name) pair for one scope.
22+ // Path is relative to cfg.ProjectDir (project scope) or homeDir (system scope).
23+ type pathEntry struct {
24+ App string
25+ Path string
26+ }
27+
28+ // projectPaths lists the unique project-scoped skill directories to scan.
29+ // Paths shared by multiple agents are collapsed into a single appGlobalSkills entry.
30+ var projectPaths = []pathEntry {
31+ // .agents/skills/ is the shared convention used by amp, kimi-cli, replit,
32+ // universal, antigravity, cline, warp, codex, cursor, deepagents,
33+ // firebender, gemini-cli, github-copilot, and opencode.
34+ {App : appGlobalSkills , Path : ".agents/skills" },
35+ // Agent-specific project paths below.
36+ {App : "augment" , Path : ".augment/skills" },
37+ {App : "bob" , Path : ".bob/skills" },
38+ {App : "claude-code" , Path : ".claude/skills" },
39+ {App : "codebuddy" , Path : ".codebuddy/skills" },
40+ {App : "command-code" , Path : ".commandcode/skills" },
41+ {App : "continue" , Path : ".continue/skills" },
42+ {App : "cortex" , Path : ".cortex/skills" },
43+ {App : "crush" , Path : ".crush/skills" },
44+ {App : "droid" , Path : ".factory/skills" },
45+ {App : "goose" , Path : ".goose/skills" },
46+ {App : "iflow-cli" , Path : ".iflow/skills" },
47+ {App : "junie" , Path : ".junie/skills" },
48+ {App : "kilo" , Path : ".kilocode/skills" },
49+ {App : "kiro-cli" , Path : ".kiro/skills" },
50+ {App : "kode" , Path : ".kode/skills" },
51+ {App : "mcpjam" , Path : ".mcpjam/skills" },
52+ {App : "mistral-vibe" , Path : ".vibe/skills" },
53+ {App : "mux" , Path : ".mux/skills" },
54+ {App : "openclaw" , Path : "skills" },
55+ {App : "openhands" , Path : ".openhands/skills" },
56+ {App : "pi" , Path : ".pi/skills" },
57+ {App : "qoder" , Path : ".qoder/skills" },
58+ {App : "qwen-code" , Path : ".qwen/skills" },
59+ {App : "roo" , Path : ".roo/skills" },
60+ // trae and trae-cn share .trae/skills/ at project scope.
61+ {App : "trae" , Path : ".trae/skills" },
62+ {App : "windsurf" , Path : ".windsurf/skills" },
63+ {App : "zencoder" , Path : ".zencoder/skills" },
64+ {App : "neovate" , Path : ".neovate/skills" },
65+ {App : "pochi" , Path : ".pochi/skills" },
66+ {App : "adal" , Path : ".adal/skills" },
2067}
2168
22- // agentRegistry is the canonical mapping of agents to their skill directories.
23- // Derived from the supported-agents table in the agent skills scanner design doc.
24- var agentRegistry = []agentEntry {
25- {App : "amp" , ProjectPath : ".agents/skills" , GlobalPath : ".config/agents/skills" },
26- {App : "kimi-cli" , ProjectPath : ".agents/skills" , GlobalPath : ".config/agents/skills" },
27- {App : "replit" , ProjectPath : ".agents/skills" , GlobalPath : ".config/agents/skills" },
28- {App : "universal" , ProjectPath : ".agents/skills" , GlobalPath : ".config/agents/skills" },
29- {App : "antigravity" , ProjectPath : ".agents/skills" , GlobalPath : ".gemini/antigravity/skills" },
30- {App : "augment" , ProjectPath : ".augment/skills" , GlobalPath : ".augment/skills" },
31- {App : "bob" , ProjectPath : ".bob/skills" , GlobalPath : ".bob/skills" },
32- {App : "claude-code" , ProjectPath : ".claude/skills" , GlobalPath : ".claude/skills" },
33- {App : "openclaw" , ProjectPath : "skills" , GlobalPath : ".openclaw/skills" },
34- {App : "cline" , ProjectPath : ".agents/skills" , GlobalPath : ".agents/skills" },
35- {App : "warp" , ProjectPath : ".agents/skills" , GlobalPath : ".agents/skills" },
36- {App : "codebuddy" , ProjectPath : ".codebuddy/skills" , GlobalPath : ".codebuddy/skills" },
37- {App : "codex" , ProjectPath : ".agents/skills" , GlobalPath : ".codex/skills" },
38- {App : "command-code" , ProjectPath : ".commandcode/skills" , GlobalPath : ".commandcode/skills" },
39- {App : "continue" , ProjectPath : ".continue/skills" , GlobalPath : ".continue/skills" },
40- {App : "cortex" , ProjectPath : ".cortex/skills" , GlobalPath : ".snowflake/cortex/skills" },
41- {App : "crush" , ProjectPath : ".crush/skills" , GlobalPath : ".config/crush/skills" },
42- {App : "cursor" , ProjectPath : ".agents/skills" , GlobalPath : ".cursor/skills" },
43- {App : "deepagents" , ProjectPath : ".agents/skills" , GlobalPath : ".deepagents/agent/skills" },
44- {App : "droid" , ProjectPath : ".factory/skills" , GlobalPath : ".factory/skills" },
45- {App : "firebender" , ProjectPath : ".agents/skills" , GlobalPath : ".firebender/skills" },
46- {App : "gemini-cli" , ProjectPath : ".agents/skills" , GlobalPath : ".gemini/skills" },
47- {App : "github-copilot" , ProjectPath : ".agents/skills" , GlobalPath : ".copilot/skills" },
48- {App : "goose" , ProjectPath : ".goose/skills" , GlobalPath : ".config/goose/skills" },
49- {App : "junie" , ProjectPath : ".junie/skills" , GlobalPath : ".junie/skills" },
50- {App : "iflow-cli" , ProjectPath : ".iflow/skills" , GlobalPath : ".iflow/skills" },
51- {App : "kilo" , ProjectPath : ".kilocode/skills" , GlobalPath : ".kilocode/skills" },
52- {App : "kiro-cli" , ProjectPath : ".kiro/skills" , GlobalPath : ".kiro/skills" },
53- {App : "kode" , ProjectPath : ".kode/skills" , GlobalPath : ".kode/skills" },
54- {App : "mcpjam" , ProjectPath : ".mcpjam/skills" , GlobalPath : ".mcpjam/skills" },
55- {App : "mistral-vibe" , ProjectPath : ".vibe/skills" , GlobalPath : ".vibe/skills" },
56- {App : "mux" , ProjectPath : ".mux/skills" , GlobalPath : ".mux/skills" },
57- {App : "opencode" , ProjectPath : ".agents/skills" , GlobalPath : ".config/opencode/skills" },
58- {App : "openhands" , ProjectPath : ".openhands/skills" , GlobalPath : ".openhands/skills" },
59- {App : "pi" , ProjectPath : ".pi/skills" , GlobalPath : ".pi/agent/skills" },
60- {App : "qoder" , ProjectPath : ".qoder/skills" , GlobalPath : ".qoder/skills" },
61- {App : "qwen-code" , ProjectPath : ".qwen/skills" , GlobalPath : ".qwen/skills" },
62- {App : "roo" , ProjectPath : ".roo/skills" , GlobalPath : ".roo/skills" },
63- {App : "trae" , ProjectPath : ".trae/skills" , GlobalPath : ".trae/skills" },
64- {App : "trae-cn" , ProjectPath : ".trae/skills" , GlobalPath : ".trae-cn/skills" },
65- {App : "windsurf" , ProjectPath : ".windsurf/skills" , GlobalPath : ".codeium/windsurf/skills" },
66- {App : "zencoder" , ProjectPath : ".zencoder/skills" , GlobalPath : ".zencoder/skills" },
67- {App : "neovate" , ProjectPath : ".neovate/skills" , GlobalPath : ".neovate/skills" },
68- {App : "pochi" , ProjectPath : ".pochi/skills" , GlobalPath : ".pochi/skills" },
69- {App : "adal" , ProjectPath : ".adal/skills" , GlobalPath : ".adal/skills" },
69+ // globalPaths lists the unique system-scoped (home-relative) skill directories
70+ // to scan. Paths shared by multiple agents are collapsed into appGlobalSkills.
71+ var globalPaths = []pathEntry {
72+ // ~/.config/agents/skills/ is shared by amp, kimi-cli, replit, universal.
73+ {App : appGlobalSkills , Path : ".config/agents/skills" },
74+ // ~/.agents/skills/ is shared by cline and warp.
75+ {App : appGlobalSkills , Path : ".agents/skills" },
76+ // Agent-specific global paths below.
77+ {App : "antigravity" , Path : ".gemini/antigravity/skills" },
78+ {App : "augment" , Path : ".augment/skills" },
79+ {App : "bob" , Path : ".bob/skills" },
80+ {App : "claude-code" , Path : ".claude/skills" },
81+ {App : "codebuddy" , Path : ".codebuddy/skills" },
82+ {App : "codex" , Path : ".codex/skills" },
83+ {App : "command-code" , Path : ".commandcode/skills" },
84+ {App : "continue" , Path : ".continue/skills" },
85+ {App : "cortex" , Path : ".snowflake/cortex/skills" },
86+ {App : "crush" , Path : ".config/crush/skills" },
87+ {App : "cursor" , Path : ".cursor/skills" },
88+ {App : "deepagents" , Path : ".deepagents/agent/skills" },
89+ {App : "droid" , Path : ".factory/skills" },
90+ {App : "firebender" , Path : ".firebender/skills" },
91+ {App : "gemini-cli" , Path : ".gemini/skills" },
92+ {App : "github-copilot" , Path : ".copilot/skills" },
93+ {App : "goose" , Path : ".config/goose/skills" },
94+ {App : "iflow-cli" , Path : ".iflow/skills" },
95+ {App : "junie" , Path : ".junie/skills" },
96+ {App : "kilo" , Path : ".kilocode/skills" },
97+ {App : "kiro-cli" , Path : ".kiro/skills" },
98+ {App : "kode" , Path : ".kode/skills" },
99+ {App : "mcpjam" , Path : ".mcpjam/skills" },
100+ {App : "mistral-vibe" , Path : ".vibe/skills" },
101+ {App : "mux" , Path : ".mux/skills" },
102+ {App : "openclaw" , Path : ".openclaw/skills" },
103+ {App : "opencode" , Path : ".config/opencode/skills" },
104+ {App : "openhands" , Path : ".openhands/skills" },
105+ {App : "pi" , Path : ".pi/agent/skills" },
106+ {App : "qoder" , Path : ".qoder/skills" },
107+ {App : "qwen-code" , Path : ".qwen/skills" },
108+ {App : "roo" , Path : ".roo/skills" },
109+ {App : "trae" , Path : ".trae/skills" },
110+ {App : "trae-cn" , Path : ".trae-cn/skills" },
111+ {App : "windsurf" , Path : ".codeium/windsurf/skills" },
112+ {App : "zencoder" , Path : ".zencoder/skills" },
113+ {App : "neovate" , Path : ".neovate/skills" },
114+ {App : "pochi" , Path : ".pochi/skills" },
115+ {App : "adal" , Path : ".adal/skills" },
70116}
71117
72118type adapter struct {}
73119
74120// New constructs an inventory.Scanner that discovers agent skill directories
75- // for all agents in the agentRegistry .
121+ // for all supported agents .
76122func New () inventory.Scanner {
77123 return & adapter {}
78124}
79125
80126// Name returns the stable scanner identifier used in logs and ScanError.
81127func (a * adapter ) Name () string { return scannerName }
82128
83- // Scan walks each agent's project-local and global skill directories,
84- // emitting one Item per subdirectory found.
129+ // Scan walks each entry in projectPaths (when project scope is enabled) and
130+ // globalPaths (when system scope is enabled), emitting one Item per skill
131+ // subdirectory found.
85132func (a * adapter ) Scan (ctx context.Context , cfg inventory.ScanConfig , emit inventory.EmitFunc ) error {
86- homeDir := cfg .HomeDir
87- if homeDir == "" {
88- if h , err := os .UserHomeDir (); err == nil {
89- homeDir = h
133+ if cfg .ScopeEnabled (inventory .ScopeProject ) && cfg .ProjectDir != "" {
134+ for _ , e := range projectPaths {
135+ dir := filepath .Join (cfg .ProjectDir , e .Path )
136+ if err := scanDir (ctx , dir , e .App , "" , inventory .ScopeProject , emit ); err != nil {
137+ return err
138+ }
90139 }
91140 }
92141
93- for _ , entry := range agentRegistry {
94- if cfg . ScopeEnabled ( inventory . ScopeProject ) && cfg .ProjectDir != "" {
95- dir := filepath . Join ( cfg . ProjectDir , entry . ProjectPath )
96- if err := scanDir ( ctx , dir , entry . App , inventory . ScopeProject , emit ); err ! = nil {
97- return err
142+ if cfg . ScopeEnabled ( inventory . ScopeSystem ) {
143+ homeDir := cfg .HomeDir
144+ if homeDir == "" {
145+ if h , err := os . UserHomeDir ( ); err = = nil {
146+ homeDir = h
98147 }
99148 }
100- if cfg .ScopeEnabled (inventory .ScopeSystem ) && homeDir != "" {
101- dir := filepath .Join (homeDir , entry .GlobalPath )
102- if err := scanDir (ctx , dir , entry .App , inventory .ScopeSystem , emit ); err != nil {
149+ if homeDir != "" {
150+ for _ , e := range globalPaths {
151+ dir := filepath .Join (homeDir , e .Path )
152+ if err := scanDir (ctx , dir , e .App , "" , inventory .ScopeSystem , emit ); err != nil {
153+ return err
154+ }
155+ }
156+ if err := scanClaudePluginSkills (ctx , homeDir , inventory .ScopeSystem , emit ); err != nil {
103157 return err
104158 }
159+ if err := scanClaudeMarketplaceSkills (ctx , homeDir , inventory .ScopeSystem , emit ); err != nil {
160+ return err
161+ }
162+ }
163+ }
164+
165+ return nil
166+ }
167+
168+ // scanClaudePluginSkills globs ~/.claude/plugins/cache/<org>/<plugin>/<version>/skills/
169+ // App is "claude-code"; Name is "<plugin>/<skill-name>".
170+ func scanClaudePluginSkills (ctx context.Context , homeDir string , scope inventory.Scope , emit inventory.EmitFunc ) error {
171+ pattern := filepath .Join (homeDir , ".claude" , "plugins" , "cache" , "*" , "*" , "*" , "skills" )
172+ matches , err := filepath .Glob (pattern )
173+ if err != nil || len (matches ) == 0 {
174+ return nil
175+ }
176+ for _ , skillsDir := range matches {
177+ // Extract plugin name: cache/<org>/<plugin>/<version>/skills → plugin is [len-3]
178+ parts := strings .Split (filepath .ToSlash (skillsDir ), "/" )
179+ if len (parts ) < 3 {
180+ continue
181+ }
182+ pluginName := parts [len (parts )- 3 ]
183+ if err := scanDir (ctx , skillsDir , "claude-code" , pluginName + "/" , scope , emit ); err != nil {
184+ return err
185+ }
186+ }
187+ return nil
188+ }
189+
190+ // scanClaudeMarketplaceSkills globs
191+ // ~/.claude/plugins/marketplaces/<marketplace>/plugins/<plugin>/skills/
192+ // App is "claude-code"; Name is "<plugin>/<skill-name>".
193+ func scanClaudeMarketplaceSkills (ctx context.Context , homeDir string , scope inventory.Scope , emit inventory.EmitFunc ) error {
194+ pattern := filepath .Join (homeDir , ".claude" , "plugins" , "marketplaces" , "*" , "plugins" , "*" , "skills" )
195+ matches , err := filepath .Glob (pattern )
196+ if err != nil || len (matches ) == 0 {
197+ return nil
198+ }
199+ for _ , skillsDir := range matches {
200+ // Extract plugin name: .../plugins/<plugin>/skills → plugin is [len-2]
201+ parts := strings .Split (filepath .ToSlash (skillsDir ), "/" )
202+ if len (parts ) < 2 {
203+ continue
204+ }
205+ pluginName := parts [len (parts )- 2 ]
206+ if err := scanDir (ctx , skillsDir , "claude-code" , pluginName + "/" , scope , emit ); err != nil {
207+ return err
105208 }
106209 }
107210 return nil
108211}
109212
110213// scanDir lists subdirectories of dir and emits one Item per directory found.
214+ // namePrefix is prepended to the skill name (e.g. "superpowers/" for plugin skills).
111215// Missing or unreadable directories are silently skipped.
112- func scanDir (_ context.Context , dir , app string , scope inventory.Scope , emit inventory.EmitFunc ) error {
216+ func scanDir (_ context.Context , dir , app , namePrefix string , scope inventory.Scope , emit inventory.EmitFunc ) error {
113217 entries , err := os .ReadDir (dir )
114218 if err != nil {
115219 return nil
@@ -120,7 +224,7 @@ func scanDir(_ context.Context, dir, app string, scope inventory.Scope, emit inv
120224 }
121225 s := & skill {
122226 App : app ,
123- Name : e .Name (),
227+ Name : namePrefix + e .Name (),
124228 Scope : scope ,
125229 ConfigPath : filepath .Join (dir , e .Name ()),
126230 SkillsDir : dir ,
0 commit comments