Skip to content
Kward Search API index

Class: Kward::Skills::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/skills/registry.rb

Overview

Discovers Agent Skills and reads their bounded instruction resources.

Project sources are included only when the caller marks them trusted. If duplicate skill names exist, the first source in documented precedence order wins.

Defined Under Namespace

Classes: SkillSource

Instance Method Summary collapse

Constructor Details

#initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:) ⇒ Registry

Returns a new instance of Registry.



17
18
19
20
21
22
23
24
25
# File 'lib/kward/skills/registry.rb', line 17

def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:)
  @config_dir = config_dir
  @workspace_root = workspace_root
  @project_skills_trusted = project_skills_trusted
  @skill_class = skill_class
  @max_file_bytes = max_file_bytes
  @markdown_parser = markdown_parser
  @inside_directory = inside_directory
end

Instance Method Details

#read_skill_file(name, relative_path = nil) ⇒ String

Reads a skill's main instructions or a bounded relative resource.

Absolute paths, paths escaping the skill folder, missing files, and files above the configured size limit return user-facing error strings.

Parameters:

  • name (String, #to_s)

    discovered skill name

  • relative_path (String, nil) (defaults to: nil)

    file relative to the skill directory; defaults to SKILL.md

Returns:

  • (String)

    skill content or an error message



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/kward/skills/registry.rb', line 62

def read_skill_file(name, relative_path = nil)
  skill = skills.find { |candidate| candidate.name == name.to_s }
  return "Error: unknown skill: #{name}" unless skill

  path = relative_path.to_s.empty? ? "SKILL.md" : relative_path.to_s
  return "Error: skill path must be relative" if Pathname.new(path).absolute?

  base = File.realpath(skill.folder)
  target = File.expand_path(path, base)
  real_target = File.realpath(target)
  unless @inside_directory.call(real_target, base)
    return "Error: skill path outside skill folder: #{path}"
  end
  return "Error: skill path is not a file: #{path}" unless File.file?(real_target)

  size = File.size(real_target)
  return "Error: skill file too large: #{path} (#{size} bytes)" if size > @max_file_bytes

  content = File.read(real_target)
  relative_path.to_s.empty? ? skill_content(skill, content) : content
rescue Errno::ENOENT
  "Error: skill file not found: #{path}"
rescue StandardError => e
  "Error: could not read skill file #{path}: #{e.message}"
end

#skillsArray<ConfigFiles::Skill>

Returns discovered, validated skills in precedence order.

Returns:



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/kward/skills/registry.rb', line 31

def skills
  seen = {}
  skill_sources.flat_map do |source|
    scan_source(source).filter_map do |path|
      skill = parse_skill(path)
      next unless skill

      if seen[skill.name]
        warn "Warning: skipping duplicate Kward skill #{skill.name.inspect}: #{path}"
        next
      end

      seen[skill.name] = true
      skill
    end
  end
rescue StandardError => e
  warn "Warning: skipping Kward skills: #{e.message}"
  []
end