Class: Rubino::Skills::Skill
- Inherits:
-
Object
- Object
- Rubino::Skills::Skill
- Defined in:
- lib/rubino/skills/skill.rb
Overview
Represents a single skill. Two layouts are supported:
* flat file — <dir>/<name>.md (the skill name is the basename)
* directory — <dir>/<name>/SKILL.md (the skill name is the dir name,
plus bundled files under references/ scripts/ assets/ etc.)
In both cases ‘path` points at the markdown body that carries the name/description frontmatter. Directory skills also expose `linked_files` (relative paths of bundled files) and can read a specific bundled file sandboxed to the skill’s own directory.
Constant Summary collapse
- NAME_RE =
A valid skill name: kebab-case lowercase letters/digits, <=64 chars. The single contract shared by the inline skill(create) tool, the post-turn distill job, and the installer’s path-segment allowlist.
/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
Instance Attribute Summary collapse
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#linked_files ⇒ Object
readonly
Returns the value of attribute linked_files.
-
#metadata ⇒ Object
readonly
Returns the value of attribute metadata.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.write!(dir:, name:, description:, body:) ⇒ Object
Writes a <dir>/<name>/SKILL.md from the given fields and returns its path.
-
.yaml_scalar(text) ⇒ Object
Quote the description as a one-line YAML scalar so a colon/newline in it can’t break the frontmatter.
Instance Method Summary collapse
-
#content ⇒ Object
Returns the full skill content (loaded lazily).
-
#current_linked_files ⇒ Object
Live relative paths of bundled files, recomputed from disk.
-
#directory? ⇒ Boolean
True when this skill is backed by a <name>/SKILL.md directory.
-
#initialize(path:) ⇒ Skill
constructor
A new instance of Skill.
-
#languages ⇒ Object
Languages this skill is scoped to (lower-cased tokens, e.g. [“ruby”]), parsed from the optional ‘languages:` frontmatter key.
-
#read_file(relative_path) ⇒ Object
Reads a bundled file by its relative path, sandboxed to the skill dir.
-
#summary ⇒ Object
Returns a summary for the agent to see available skills.
Constructor Details
#initialize(path:) ⇒ Skill
Returns a new instance of Skill.
60 61 62 63 64 65 66 67 68 |
# File 'lib/rubino/skills/skill.rb', line 60 def initialize(path:) @path = path @metadata = {} @content = nil @linked_files = [] @directory = directory_skill? ? File.dirname(path) : nil discover_linked_files! if directory? parse_frontmatter! end |
Instance Attribute Details
#description ⇒ Object (readonly)
Returns the value of attribute description.
18 19 20 |
# File 'lib/rubino/skills/skill.rb', line 18 def description @description end |
#linked_files ⇒ Object (readonly)
Returns the value of attribute linked_files.
18 19 20 |
# File 'lib/rubino/skills/skill.rb', line 18 def linked_files @linked_files end |
#metadata ⇒ Object (readonly)
Returns the value of attribute metadata.
18 19 20 |
# File 'lib/rubino/skills/skill.rb', line 18 def @metadata end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
18 19 20 |
# File 'lib/rubino/skills/skill.rb', line 18 def name @name end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
18 19 20 |
# File 'lib/rubino/skills/skill.rb', line 18 def path @path end |
Class Method Details
.write!(dir:, name:, description:, body:) ⇒ Object
Writes a <dir>/<name>/SKILL.md from the given fields and returns its path. The single skill-writer shared by the inline skill(create) tool and the distill job: it creates the dir, assembles the frontmatter (the description quoted as a one-line YAML scalar so a colon/newline can’t break it), appends the body with a trailing newline, and writes. The caller owns everything situational — name/dup validation, the registry re-scan, the SKILL_CREATED event, any metrics — so the two paths keep their distinct side effects.
33 34 35 36 37 38 39 40 41 |
# File 'lib/rubino/skills/skill.rb', line 33 def self.write!(dir:, name:, description:, body:) FileUtils.mkdir_p(dir) path = File.join(dir, "SKILL.md") content = "---\nname: #{name}\ndescription: #{yaml_scalar(description)}\n---\n\n" content << body content << "\n" unless content.end_with?("\n") File.write(path, content) path end |
.yaml_scalar(text) ⇒ Object
Quote the description as a one-line YAML scalar so a colon/newline in it can’t break the frontmatter.
45 46 47 48 |
# File 'lib/rubino/skills/skill.rb', line 45 def self.yaml_scalar(text) one_line = text.to_s.tr("\n", " ").strip %("#{one_line.gsub('"', '\\"')}") end |
Instance Method Details
#content ⇒ Object
Returns the full skill content (loaded lazily)
76 77 78 |
# File 'lib/rubino/skills/skill.rb', line 76 def content @content ||= load_content end |
#current_linked_files ⇒ Object
Live relative paths of bundled files, recomputed from disk. Unlike the linked_files snapshot taken at init, this reflects the current dir state — so an error message built from it can’t list a file that #read_file just failed to find (the W3 self-contradiction). Empty for flat-file skills.
105 106 107 108 109 |
# File 'lib/rubino/skills/skill.rb', line 105 def current_linked_files return [] unless directory? collect_linked_files end |
#directory? ⇒ Boolean
True when this skill is backed by a <name>/SKILL.md directory.
71 72 73 |
# File 'lib/rubino/skills/skill.rb', line 71 def directory? !@directory.nil? end |
#languages ⇒ Object
Languages this skill is scoped to (lower-cased tokens, e.g. [“ruby”]), parsed from the optional ‘languages:` frontmatter key. Empty means the skill is language-agnostic and always surfaced. A scoped skill is only auto-listed in the system-prompt catalogue when the project uses one of its languages — see Registry#summaries — so a Ruby skill no longer brands a Python project. It stays discoverable/loadable on demand.
56 57 58 |
# File 'lib/rubino/skills/skill.rb', line 56 def languages Array(@metadata["languages"]).map { |l| l.to_s.strip.downcase }.reject(&:empty?) end |
#read_file(relative_path) ⇒ Object
Reads a bundled file by its relative path, sandboxed to the skill dir. Returns the file contents, or nil if the skill has no directory, the path escapes the skill dir, or the file does not exist.
Resolve and read happen back-to-back with no listing step in between, so the caller can’t observe a “present in the listing but unreadable” state from THIS method. A File::ENOENT between #file? and #read (the skill dir being torn down mid-call) is swallowed to nil rather than raised, so a concurrent teardown reads as a clean miss instead of a crash (W3).
89 90 91 92 93 94 95 96 97 98 |
# File 'lib/rubino/skills/skill.rb', line 89 def read_file(relative_path) return nil unless directory? resolved = resolve_within_dir(relative_path) return nil unless resolved && File.file?(resolved) File.read(resolved, encoding: "UTF-8") rescue Errno::ENOENT, Errno::EACCES nil end |
#summary ⇒ Object
Returns a summary for the agent to see available skills
112 113 114 |
# File 'lib/rubino/skills/skill.rb', line 112 def summary "#{@name}: #{@description}" end |