Class: GDKBox::Skills
- Inherits:
-
Object
- Object
- GDKBox::Skills
- Defined in:
- lib/gdkbox/skills.rb
Overview
Discovers and installs agent skills.
A skill is a directory containing a SKILL.md file. gdkbox knows about
skills from three roots: the ones bundled in this repo's top-level
skills/ directory, the user-wide ~/.claude/skills, and the current
project's ./.claude/skills. They can be installed into a host Claude
Code skills directory or pushed into a box for dispatched agents to use.
Defined Under Namespace
Classes: Skill
Constant Summary collapse
- SOURCE_DIR =
Where the bundled skills live inside the gem/repo:
/skills. File.("../../skills", __dir__)
- GLOBAL_DEST =
User-wide Claude Code skills directory.
File.("~/.claude/skills")
Instance Attribute Summary collapse
-
#dest ⇒ Object
readonly
Returns the value of attribute dest.
Class Method Summary collapse
-
.available ⇒ Object
Names of the skills shipped with this repo, sorted.
-
.description_for(path) ⇒ Object
Best-effort one-line summary pulled from a skill's SKILL.md frontmatter.
-
.discover(project_dir: Dir.pwd) ⇒ Object
Every skill gdkbox can see, across all roots, deduped by name (the first/most-specific root wins).
-
.project_dest(dir = Dir.pwd) ⇒ Object
Project-local Claude Code skills directory, relative to a working dir.
-
.resolve(token, project_dir: Dir.pwd) ⇒ Object
Resolve a token (a discovered skill name, or a path to a skill directory) to a Skill.
-
.roots(project_dir: Dir.pwd) ⇒ Object
The skill roots searched by
discover, most specific first so that a project or user skill shadows a bundled one with the same name.
Instance Method Summary collapse
-
#initialize(dest: GLOBAL_DEST) ⇒ Skills
constructor
A new instance of Skills.
-
#install(name, force: false) ⇒ Object
Copy a single named skill into the destination directory.
Constructor Details
#initialize(dest: GLOBAL_DEST) ⇒ Skills
Returns a new instance of Skills.
102 103 104 |
# File 'lib/gdkbox/skills.rb', line 102 def initialize(dest: GLOBAL_DEST) @dest = dest end |
Instance Attribute Details
#dest ⇒ Object (readonly)
Returns the value of attribute dest.
106 107 108 |
# File 'lib/gdkbox/skills.rb', line 106 def dest @dest end |
Class Method Details
.available ⇒ Object
Names of the skills shipped with this repo, sorted.
94 95 96 97 98 99 100 |
# File 'lib/gdkbox/skills.rb', line 94 def self.available return [] unless File.directory?(SOURCE_DIR) Dir.children(SOURCE_DIR) .select { |entry| File.directory?(File.join(SOURCE_DIR, entry)) } .sort end |
.description_for(path) ⇒ Object
Best-effort one-line summary pulled from a skill's SKILL.md frontmatter.
79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/gdkbox/skills.rb', line 79 def self.description_for(path) text = File.read(File.join(path, "SKILL.md")) front = text[/\A---\s*\n(.*?)\n---\s*\n/m, 1] return nil unless front data = begin YAML.safe_load(front) rescue StandardError nil end desc = data.is_a?(Hash) ? data["description"] : nil desc&.to_s&.gsub(/\s+/, " ")&.strip end |
.discover(project_dir: Dir.pwd) ⇒ Object
Every skill gdkbox can see, across all roots, deduped by name (the first/most-specific root wins). Returns Skill structs sorted by name.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/gdkbox/skills.rb', line 41 def self.discover(project_dir: Dir.pwd) found = {} roots(project_dir: project_dir).each do |origin, dir| next unless File.directory?(dir) Dir.children(dir).sort.each do |entry| path = File.join(dir, entry) next if found.key?(entry) next unless File.file?(File.join(path, "SKILL.md")) found[entry] = Skill.new(entry, path, origin, description_for(path)) end end found.values.sort_by(&:name) end |
.project_dest(dir = Dir.pwd) ⇒ Object
Project-local Claude Code skills directory, relative to a working dir.
25 26 27 |
# File 'lib/gdkbox/skills.rb', line 25 def self.project_dest(dir = Dir.pwd) File.join(dir, ".claude", "skills") end |
.resolve(token, project_dir: Dir.pwd) ⇒ Object
Resolve a token (a discovered skill name, or a path to a skill directory) to a Skill. Raises Error when it cannot be found.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/gdkbox/skills.rb', line 59 def self.resolve(token, project_dir: Dir.pwd) maybe_path = File.(token) if token.match?(%r{[/\\]}) || File.directory?(maybe_path) unless File.file?(File.join(maybe_path, "SKILL.md")) raise Error, "'#{token}' is not a skill directory (no SKILL.md)." end return Skill.new(File.basename(maybe_path), maybe_path, :path, description_for(maybe_path)) end skill = discover(project_dir: project_dir).find { |s| s.name == token } unless skill names = discover(project_dir: project_dir).map(&:name) hint = names.empty? ? "" : " Available: #{names.join(', ')}." raise Error, "No skill named '#{token}'.#{hint}" end skill end |
.roots(project_dir: Dir.pwd) ⇒ Object
The skill roots searched by discover, most specific first so that a
project or user skill shadows a bundled one with the same name.
31 32 33 34 35 36 37 |
# File 'lib/gdkbox/skills.rb', line 31 def self.roots(project_dir: Dir.pwd) [ [:project, project_dest(project_dir)], [:global, GLOBAL_DEST], [:bundled, SOURCE_DIR] ] end |
Instance Method Details
#install(name, force: false) ⇒ Object
Copy a single named skill into the destination directory.
Raises Error if the skill is unknown, or if it is already installed and
force is false. Returns the path it was installed to.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/gdkbox/skills.rb', line 112 def install(name, force: false) source = File.join(SOURCE_DIR, name) raise Error, "No skill named '#{name}' in this repo." unless File.directory?(source) target = File.join(@dest, name) if File.exist?(target) && !force raise Error, "Skill '#{name}' is already installed at #{target}. Use --force to overwrite." end FileUtils.mkdir_p(@dest) FileUtils.rm_rf(target) FileUtils.cp_r(source, target) target end |