Class: LittleGhost::Skills::Catalog
- Inherits:
-
Object
- Object
- LittleGhost::Skills::Catalog
- Includes:
- Enumerable
- Defined in:
- lib/little_ghost/skills/catalog.rb
Overview
A Catalog lets an agent discover focused instructions without putting every skill in its prompt. The model sees short descriptions first and can load a skill's full instructions when the task calls for them.
catalog = LittleGhost::Skills::Catalog.new(paths: ["app/skills"])
catalog.names # => ["refund_policy", "search_orders"]
catalog.discovery_prompt.include?("refund_policy") # => true
catalog.tool # a LittleGhost::Tool that loads full instructions on demand
Each immediate child directory may contain one SKILL.md with YAML front
matter. Symbolic-link escapes, unsafe names, oversized files, and invalid
YAML are rejected or skipped before instructions reach a model. Optional
resource listings are limited by count and depth.
Security and trust
Configured roots and their contents are fully trusted instruction sources.
The allowed-tools field is metadata shown to the model, not an
authorization boundary. Applications must enforce tool access separately
and keep skill roots non-user-writable.
Constant Summary collapse
- DEFAULT_MAX_SKILLS =
:nodoc:
1_000- DEFAULT_MAX_FILE_BYTES =
:nodoc:
1_000_000- DEFAULT_MAX_RESOURCE_FILES =
:nodoc:
20- MAX_RESOURCE_DEPTH =
:nodoc:
3- RESOURCE_DIRECTORIES =
:nodoc:
%w[scripts references assets].freeze
- SAFE_NAME_PATTERN =
:nodoc:
/\A[a-zA-Z0-9_-]+\z/
Instance Method Summary collapse
-
#discovery_prompt ⇒ Object
Produces the escaped, metadata-only prompt used for discovery.
-
#each(&block) ⇒ Object
Yields each Skill in lookup order.
-
#fetch(name) ⇒ Object
Finds the named Skill or raises ConfigurationError.
-
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
-
#initialize(paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil) ⇒ Catalog
constructor
Loads valid skills immediately using the supplied safety limits.
-
#names ⇒ Object
Lists immutable skill names in lookup order.
-
#tool ⇒ Object
Exposes full instructions on demand through a
skillsTool.
Constructor Details
#initialize(paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil) ⇒ Catalog
Loads valid skills immediately using the supplied safety limits.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/little_ghost/skills/catalog.rb', line 43 def initialize( paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil ) @paths = PathSet.new(paths) @max_skills = positive_integer(max_skills, :max_skills) @max_file_bytes = positive_integer(max_file_bytes, :max_file_bytes) @max_resource_files = positive_integer(max_resource_files, :max_resource_files) @only = Array(only).map(&:to_s).freeze if only @resource_root = canonical_resource_root(resource_root) @skills = load_skills end |
Instance Method Details
#discovery_prompt ⇒ Object
Produces the escaped, metadata-only prompt used for discovery.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/little_ghost/skills/catalog.rb', line 76 def discovery_prompt return "" if @skills.empty? lines = ["<available_skills>"] @skills.each_value do |skill| lines.concat([ "<skill>", "<name>#{ERB::Util.html_escape(skill.name)}</name>", "<description>#{ERB::Util.html_escape(skill.description)}</description>", "<location>#{ERB::Util.html_escape(skill.path)}</location>", "</skill>" ]) end lines << "</available_skills>" lines.join("\n") end |
#each(&block) ⇒ Object
Yields each Skill in lookup order.
61 62 63 |
# File 'lib/little_ghost/skills/catalog.rb', line 61 def each(&block) @skills.each_value(&block) end |
#fetch(name) ⇒ Object
Finds the named Skill or raises ConfigurationError.
66 67 68 |
# File 'lib/little_ghost/skills/catalog.rb', line 66 def fetch(name) @skills.fetch(name.to_s) { raise ConfigurationError, "Unknown skill: #{name}" } end |
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/little_ghost/skills/catalog.rb', line 119 def format(skill) parts = [skill.instructions] = [] << "Allowed tools: #{skill.allowed_tools.join(", ")}" unless skill.allowed_tools.empty? << "Compatibility: #{skill.compatibility}" if skill.compatibility << "Location: #{skill.path}" parts << "\n---\n#{.join("\n")}" unless .empty? resources = skill_resources(skill) unless resources.empty? parts << "\nAvailable resources:\n#{resources.map { |path| " #{path}" }.join("\n")}" end parts.join("\n") end |
#names ⇒ Object
Lists immutable skill names in lookup order.
71 72 73 |
# File 'lib/little_ghost/skills/catalog.rb', line 71 def names @skills.keys.freeze end |
#tool ⇒ Object
Exposes full instructions on demand through a skills Tool.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/little_ghost/skills/catalog.rb', line 94 def tool catalog = self Tool.define( name: "skills", description: <<~DESCRIPTION.strip, Activate a skill to load its full instructions. Use this tool to load the complete instructions for a skill listed in the available_skills section of your system prompt. DESCRIPTION input_schema: { type: "object", properties: {skill_name: {type: "string", description: "Name of the skill to activate."}}, required: ["skill_name"], additionalProperties: false } ) do |input| catalog.format(catalog.fetch(input.fetch("skill_name"))) rescue ConfigurationError => error raise ToolError, error. end end |