Class: Octo::SkillLoader
- Inherits:
-
Object
- Object
- Octo::SkillLoader
- Defined in:
- lib/octo/skill_loader.rb
Overview
Loader and registry for skills. Discovers skills from multiple locations and provides lookup functionality.
Constant Summary collapse
- LOCATIONS =
Skill discovery locations (in priority order: lower index = lower priority)
[ :default, # gem's built-in default skills (lowest priority) :global_octo, # ~/.octo/skills/ :project_octo # .octo/skills/ (highest priority) ].freeze
Instance Method Summary collapse
-
#[](identifier) ⇒ Skill?
Get a skill by its identifier.
-
#all_skills ⇒ Array<Skill>
Get all loaded skills.
- #build_skill_content(frontmatter, content) ⇒ Object
-
#clear ⇒ Object
Clear loaded skills and errors.
-
#count ⇒ Integer
Get the count of loaded skills.
-
#create_skill(name, content, description = nil, location: :global) ⇒ Skill
Create a new skill directory and SKILL.md file.
-
#delete_skill(name) ⇒ Boolean
Delete a skill.
-
#errors ⇒ Array<String>
Get loading errors.
-
#find_by_command(command) ⇒ Skill?
Find a skill by its slash command.
-
#find_by_name(name) ⇒ Skill?
Find a skill by its name (identifier).
-
#initialize(working_dir:) ⇒ SkillLoader
constructor
Initialize the skill loader and automatically load all skills.
-
#load_all ⇒ Array<Skill>
Load all skills from configured locations Clears previously loaded skills before loading to ensure idempotency.
-
#load_global_octo_skills ⇒ Array<Skill>
Load skills from ~/.octo/skills/ (user global).
-
#load_project_octo_skills ⇒ Array<Skill>
Load skills from .octo/skills/ (project-level, highest priority).
- #load_skills_from_directory(dir, source_type) ⇒ Object
-
#loaded_from ⇒ Hash{String => Symbol}
Get the source location for each loaded skill.
-
#toggle_skill(name, enabled:) ⇒ Skill
Toggle a skill’s disable-model-invocation field in its SKILL.md.
-
#user_invocable_skills ⇒ Array<Skill>
Get skills that can be invoked by user.
Constructor Details
#initialize(working_dir:) ⇒ SkillLoader
Initialize the skill loader and automatically load all skills
22 23 24 25 26 27 28 29 30 |
# File 'lib/octo/skill_loader.rb', line 22 def initialize(working_dir:) @working_dir = working_dir @skills = {} # Map identifier -> Skill @skills_by_command = {} # Map slash_command -> Skill @errors = [] # Store loading errors @loaded_from = {} # Track which location each skill was loaded from load_all end |
Instance Method Details
#[](identifier) ⇒ Skill?
Get a skill by its identifier
75 76 77 |
# File 'lib/octo/skill_loader.rb', line 75 def [](identifier) @skills[identifier] end |
#all_skills ⇒ Array<Skill>
Get all loaded skills
68 69 70 |
# File 'lib/octo/skill_loader.rb', line 68 def all_skills @skills.values end |
#build_skill_content(frontmatter, content) ⇒ Object
298 299 300 301 302 303 304 |
# File 'lib/octo/skill_loader.rb', line 298 def build_skill_content(frontmatter, content) yaml = frontmatter .reject { |_, v| v.nil? || v.to_s.empty? } .to_yaml(line_width: 80) "---\n#{yaml}---\n\n#{content}" end |
#clear ⇒ Object
Clear loaded skills and errors
118 119 120 121 122 123 |
# File 'lib/octo/skill_loader.rb', line 118 def clear @skills.clear @skills_by_command.clear @loaded_from.clear @errors.clear end |
#count ⇒ Integer
Get the count of loaded skills
101 102 103 |
# File 'lib/octo/skill_loader.rb', line 101 def count @skills.size end |
#create_skill(name, content, description = nil, location: :global) ⇒ Skill
Create a new skill directory and SKILL.md file
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/octo/skill_loader.rb', line 131 def create_skill(name, content, description = nil, location: :global) # Validate name unless name.match?(/^[a-z0-9][a-z0-9-]*$/) raise Octo::AgentError, "Invalid skill name '#{name}'. Use lowercase letters, numbers, and hyphens only." end # Determine directory path skill_dir = case location when :global Pathname.new(ENV.fetch("HOME", "~")).join(".octo", "skills", name) when :project Pathname.new(@working_dir).join(".octo", "skills", name) else raise Octo::AgentError, "Unknown skill location: #{location}" end # Create directory if it doesn't exist FileUtils.mkdir_p(skill_dir) # Build frontmatter frontmatter = { "name" => name, "description" => description } # Write SKILL.md skill_content = build_skill_content(frontmatter, content) skill_file = skill_dir.join("SKILL.md") skill_file.write(skill_content) # Load the newly created skill source_type = case location when :global then :global_octo when :project then :project_octo else :global_octo end load_single_skill(skill_dir, skill_dir, name, source_type) end |
#delete_skill(name) ⇒ Boolean
Delete a skill
199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/octo/skill_loader.rb', line 199 def delete_skill(name) skill = @skills[name] return false unless skill # Remove from registry @skills.delete(name) @skills_by_command.delete(skill.slash_command) # Delete directory FileUtils.rm_rf(skill.directory) true end |
#errors ⇒ Array<String>
Get loading errors
107 108 109 |
# File 'lib/octo/skill_loader.rb', line 107 def errors @errors.dup end |
#find_by_command(command) ⇒ Skill?
Find a skill by its slash command
82 83 84 |
# File 'lib/octo/skill_loader.rb', line 82 def find_by_command(command) @skills_by_command[command] end |
#find_by_name(name) ⇒ Skill?
Find a skill by its name (identifier)
89 90 91 |
# File 'lib/octo/skill_loader.rb', line 89 def find_by_name(name) @skills[name] end |
#load_all ⇒ Array<Skill>
Load all skills from configured locations Clears previously loaded skills before loading to ensure idempotency
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/octo/skill_loader.rb', line 35 def load_all # Clear existing skills to ensure idempotent reloading clear load_default_skills load_global_octo_skills # Only load project-level skills when working_dir is explicitly provided. # When nil (e.g. WebUI server mode), skip project skills to keep the loader # project-agnostic and only expose global skills. if @working_dir load_project_octo_skills end all_skills end |
#load_global_octo_skills ⇒ Array<Skill>
Load skills from ~/.octo/skills/ (user global)
54 55 56 57 |
# File 'lib/octo/skill_loader.rb', line 54 def load_global_octo_skills global_octo_dir = Pathname.new(ENV.fetch("HOME", "~")).join(".octo", "skills") load_skills_from_directory(global_octo_dir, :global_octo) end |
#load_project_octo_skills ⇒ Array<Skill>
Load skills from .octo/skills/ (project-level, highest priority)
61 62 63 64 |
# File 'lib/octo/skill_loader.rb', line 61 def load_project_octo_skills project_octo_dir = Pathname.new(@working_dir).join(".octo", "skills") load_skills_from_directory(project_octo_dir, :project_octo) end |
#load_skills_from_directory(dir, source_type) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/octo/skill_loader.rb', line 214 def load_skills_from_directory(dir, source_type) return [] unless dir.exist? source_path = case source_type when :global_octo Pathname.new(ENV.fetch("HOME", "~")).join(".octo") when :project_octo Pathname.new(@working_dir) else dir end skills = [] dir.children.select(&:directory?).each do |entry| if entry.join("SKILL.md").exist? # Direct skill directory skill = load_single_skill(entry, source_path, entry.basename.to_s, source_type) skills << skill if skill else # Treat as a category directory — scan one level deeper for skills. # This allows grouping skills under ~/.octo/skills/<category>/<skill>/SKILL.md # (e.g. openclaw-imports/my-skill/SKILL.md) without changing the loader contract. entry.children.select(&:directory?).each do |skill_dir| next unless skill_dir.join("SKILL.md").exist? skill = load_single_skill(skill_dir, source_path, skill_dir.basename.to_s, source_type) skills << skill if skill end end end skills end |
#loaded_from ⇒ Hash{String => Symbol}
Get the source location for each loaded skill
113 114 115 |
# File 'lib/octo/skill_loader.rb', line 113 def loaded_from @loaded_from.dup end |
#toggle_skill(name, enabled:) ⇒ Skill
Toggle a skill’s disable-model-invocation field in its SKILL.md. System skills (source: :default) cannot be toggled — raises AgentError.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/octo/skill_loader.rb', line 173 def toggle_skill(name, enabled:) skill = @skills[name] raise Octo::AgentError, "Skill not found: #{name}" unless skill raise Octo::AgentError, "Cannot toggle system skill: #{name}" if @loaded_from[name] == :default skill_file = skill.directory.join("SKILL.md") fm = (skill.frontmatter || {}).dup if enabled fm["disable-model-invocation"] = false else fm["disable-model-invocation"] = true end skill_file.write(build_skill_content(fm, skill.content)) # Reload into registry reloaded = Skill.new(skill.directory, source_path: skill.source_path) @skills[reloaded.identifier] = reloaded @skills_by_command[reloaded.slash_command] = reloaded reloaded end |
#user_invocable_skills ⇒ Array<Skill>
Get skills that can be invoked by user
95 96 97 |
# File 'lib/octo/skill_loader.rb', line 95 def user_invocable_skills all_skills.select(&:user_invocable?) end |