ruby_llm-skills 0.2.0 → 0.4.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 676c58cfd47ff3e3bbf9a8eeac47658c56f2537d189eef7f2743740b8c44afb8
4
- data.tar.gz: b54d6857a8340b9aab46a37e6385296acd6190987ef816172f96b265a4378d72
3
+ metadata.gz: ebbccd1655baa8f141a020e2a32fdad03f8c6ddc7cd8929cb6b35cee8e4fc488
4
+ data.tar.gz: b1d175a5fc1ec6c7603318a42fdb34d44f25680da95fa21a2b3f042621644641
5
5
  SHA512:
6
- metadata.gz: 22da0df8891691181c0db718da07e3b44ad1ca1cbd04d3991350e5f7fff9996390493a5f047833ff978b8798f447172cfc7f7ddbdffe1871054e61cea40709f0
7
- data.tar.gz: 0a48577bdb85af0d6a1216857fbdb9e4389a2121f271bdc48cff4f35324be2854563b8934d62064667fd1b829959e3e595c05d8be7bd5475b7d6797a36cf992d
6
+ metadata.gz: 1b39b6966015406944f68897ed6170bce39cbd6f36ad6d363e650a1dbfc160a99a532e728b1e599d3b5243a4323db6c24220e79c20f796efa3c714a45126140d
7
+ data.tar.gz: eb71f38a6faa4c75faf675f9c6ce7ac7ae835db82a1a58c1d1c5c2b5ec5511426fc7c5e770ec91e1ad1019e8868c81a49ef65ae254e8cf4256e66f0348c87d41
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0.pre1] - 2026-09-10
9
+
10
+ ### Changed
11
+
12
+ - **Breaking:** requires RubyLLM 2.0 (`ruby_llm >= 2.0.0.rc2, < 3`); 1.x users should stay on 0.3.x
13
+ - `SkillTool` declares parameters with the 2.0 `parameter`/`description:` DSL, exposes `parameters_schema`, and sets its name through `tool_name`
14
+ - `Chat#with_skills` registers the skill tool via `Chat#with_tools` (2.0 removed `Chat#with_tool`)
15
+ - `AgentExtensions` hooks the 2.0 `Agent.apply_configuration(chat, input_values:, persist_instructions:)` signature and applies skills to the chat or `acts_as_chat` record it receives
16
+ - Integration tests pin `openai_protocol = :chat_completions` and match VCR cassettes on method and URI so the recorded 1.x interactions replay under 2.0
17
+
18
+ ### Removed
19
+
20
+ - Global `Module#delegate` fallback and its compatibility test (2.0's `Agent` uses `Forwardable`)
21
+ - Dependency on the removed `Agent.llm_chat_for` hook
22
+
23
+ ### Fixed
24
+
25
+ - StandardRB `Layout/EmptyLinesAroundModuleBody` offense in `chat_extensions.rb` that failed CI
26
+
27
+ ## [0.3.0] - 2026-02-17
28
+
29
+ ### Added
30
+
31
+ - `RubyLLM::Agent` integration via `RubyLLM::Skills::AgentExtensions`
32
+ - Class-level `skills` DSL on agent subclasses with source, `only:`, and proc support
33
+ - Instance-level `with_skills` for runtime agent skill configuration
34
+ - Agent-specific unit and integration test coverage
35
+ - Compatibility tests for delegate fallback behavior
36
+
37
+ ### Changed
38
+
39
+ - Tightened `ruby_llm` dependency from open lower bound to `~> 1.12`
40
+ - Added explicit runtime compatibility checks for required `RubyLLM::Agent` hooks
41
+ - Documented `agent.with_skills(...)` replacement semantics in README
42
+
43
+ ### Fixed
44
+
45
+ - Dynamic skill blocks resolving to `nil` or `[]` no longer silently load default skills
46
+ - Skill source normalization and validation now prevent nested/invalid source runtime crashes
47
+ - Delegate fallback now supports common delegation options (`prefix`, `allow_nil`, `private`)
48
+
8
49
  ## [0.1.0] - 2025-01-15
9
50
 
10
51
  ### Added
data/README.md CHANGED
@@ -9,9 +9,12 @@ Agent Skills for [RubyLLM](https://github.com/crmne/ruby_llm). Teach your AI how
9
9
  ## Installation
10
10
 
11
11
  ```ruby
12
- gem "ruby_llm-skills"
12
+ gem "ruby_llm", "2.0.0.rc2"
13
+ gem "ruby_llm-skills", "0.4.0.pre1"
13
14
  ```
14
15
 
16
+ Requires RubyLLM 2.0 (`>= 2.0.0.rc2`). If you are still on RubyLLM 1.x, stay on `ruby_llm-skills ~> 0.3.0` and follow the [RubyLLM 2.0 upgrade guide](https://rubyllm.com/next/upgrading/) before upgrading both gems together.
17
+
15
18
  ## Quick Start
16
19
 
17
20
  ```ruby
@@ -31,6 +34,26 @@ chat.with_skills("app/skills", "app/commands") # multiple paths
31
34
  chat.with_skills("app/skills", user.skills) # with database records
32
35
  ```
33
36
 
37
+ ### With RubyLLM::Agent
38
+
39
+ ```ruby
40
+ class SupportAgent < RubyLLM::Agent
41
+ model "gpt-5-nano"
42
+ instructions "You are a support assistant."
43
+ skills "app/skills", only: [:faq, :troubleshooting]
44
+ end
45
+
46
+ chat = SupportAgent.chat
47
+ chat.ask("How do I reset my password?")
48
+
49
+ agent = SupportAgent.new
50
+ agent.with_skills("extra/skills")
51
+ agent.ask("What can you help with?")
52
+ ```
53
+
54
+ `agent.with_skills(...)` replaces the current skill tool configuration.
55
+ To combine sources, pass all sources in a single `skills`/`with_skills` call.
56
+
34
57
  ## Creating Skills
35
58
 
36
59
  ```
@@ -96,6 +119,37 @@ Default path auto-configured to `Rails.root/app/skills`.
96
119
  rails generate skill pdf-report --description "Generate PDF reports"
97
120
  ```
98
121
 
122
+ ## Development
123
+
124
+ ### Setup
125
+
126
+ ```bash
127
+ bin/setup
128
+ ```
129
+
130
+ ### Running Tests
131
+
132
+ ```bash
133
+ bundle exec rake test # Unit tests (177 tests)
134
+ bundle exec rake test_rails # Rails integration tests (25+ tests)
135
+ bundle exec rake test_all # Both
136
+ bundle exec rake # Tests + linting
137
+ ```
138
+
139
+ ### Dummy Rails App
140
+
141
+ A minimal Rails 8 app at `test/dummy/` tests Rails integration:
142
+
143
+ - **Filesystem skills**: `app/skills/greeting/` tests directory-based loading
144
+ - **Database skills**: `Skill` model tests ActiveRecord-based loading
145
+ - **Generator tests**: Tests for `rails generate skill`
146
+ - **Composite loading**: Tests combining filesystem + database sources
147
+
148
+ ```bash
149
+ cd test/dummy
150
+ bundle exec rails test # Run Rails tests directly
151
+ ```
152
+
99
153
  ## Resources
100
154
 
101
155
  - [Agent Skills Specification](https://agentskills.io/specification)
@@ -0,0 +1,29 @@
1
+ Description:
2
+ Creates a new Agent Skill following the agentskills.io specification.
3
+ Skills are stored in app/skills/ and contain a SKILL.md file with
4
+ YAML frontmatter (name, description) and markdown instructions.
5
+
6
+ Example:
7
+ rails generate skill pdf-report --description "Generate PDF reports"
8
+
9
+ This creates:
10
+ app/skills/pdf-report/
11
+ app/skills/pdf-report/SKILL.md
12
+
13
+ With optional directories:
14
+ --scripts Creates scripts/ directory for executable code
15
+ --references Creates references/ directory for documentation
16
+ --assets Creates assets/ directory for templates/images
17
+
18
+ Full Example:
19
+ rails generate skill data-export -d "Export data to CSV/JSON. Use when asked to export or download data." --scripts --assets
20
+
21
+ This creates:
22
+ app/skills/data-export/
23
+ app/skills/data-export/SKILL.md
24
+ app/skills/data-export/scripts/.keep
25
+ app/skills/data-export/assets/.keep
26
+
27
+ Note:
28
+ The description should include both what the skill does AND when to use it.
29
+ Example: "Generate PDF reports from data. Use when asked to create reports or export to PDF."
@@ -5,11 +5,16 @@ require "rails/generators"
5
5
  class SkillGenerator < Rails::Generators::NamedBase
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
- class_option :description, type: :string, default: "Description of what this skill does"
9
- class_option :license, type: :string, default: nil
10
- class_option :scripts, type: :boolean, default: false
11
- class_option :references, type: :boolean, default: false
12
- class_option :assets, type: :boolean, default: false
8
+ class_option :description, type: :string, default: "Description of what this skill does. Use when...",
9
+ aliases: "-d", desc: "Short description of the skill (max 1024 chars)"
10
+ class_option :license, type: :string, default: nil,
11
+ aliases: "-l", desc: "License identifier (e.g., MIT, Apache-2.0)"
12
+ class_option :scripts, type: :boolean, default: false,
13
+ desc: "Create scripts/ directory for executable code"
14
+ class_option :references, type: :boolean, default: false,
15
+ desc: "Create references/ directory for documentation"
16
+ class_option :assets, type: :boolean, default: false,
17
+ desc: "Create assets/ directory for templates/images"
13
18
 
14
19
  def create_skill_directory
15
20
  empty_directory skill_path
@@ -57,4 +62,8 @@ class SkillGenerator < Rails::Generators::NamedBase
57
62
  def skill_license
58
63
  options[:license]
59
64
  end
65
+
66
+ def skill_title
67
+ skill_name.split("-").map(&:capitalize).join(" ")
68
+ end
60
69
  end
@@ -6,16 +6,52 @@ license: <%= skill_license %>
6
6
  <% end -%>
7
7
  ---
8
8
 
9
- # <%= skill_name.split("-").map(&:capitalize).join(" ") %>
9
+ # <%= skill_title %>
10
10
 
11
- Instructions for using this skill.
11
+ <%= skill_description %>
12
12
 
13
13
  ## When to Use
14
14
 
15
- Use this skill when...
15
+ Use this skill when:
16
16
 
17
- ## Steps
17
+ - [Trigger condition 1 - be specific about keywords/phrases]
18
+ - [Trigger condition 2]
18
19
 
19
- 1. First step
20
- 2. Second step
21
- 3. Third step
20
+ ## Instructions
21
+
22
+ ### Step 1: [Action]
23
+
24
+ [Clear instruction with expected outcome]
25
+
26
+ ### Step 2: [Action]
27
+
28
+ [Clear instruction with expected outcome]
29
+
30
+ ### Step 3: [Action]
31
+
32
+ [Clear instruction with expected outcome]
33
+
34
+ ## Examples
35
+
36
+ ### Example 1: [Scenario]
37
+
38
+ **Input:**
39
+ ```
40
+ [Example input or command]
41
+ ```
42
+
43
+ **Output:**
44
+ ```
45
+ [Expected result]
46
+ ```
47
+
48
+ ## Edge Cases
49
+
50
+ - **[Scenario]**: [How to handle it]
51
+ - **[Error condition]**: [Recovery steps]
52
+
53
+ ## Notes
54
+
55
+ - Keep this file under 500 lines for optimal token usage
56
+ - Move detailed documentation to `references/` directory
57
+ - Move executable code to `scripts/` directory
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "source_detection"
4
+
5
+ module RubyLLM
6
+ module Skills
7
+ # Extensions for RubyLLM::Agent to enable declarative skill configuration.
8
+ #
9
+ # @example Static skills
10
+ # class SupportAgent < RubyLLM::Agent
11
+ # skills "app/skills", only: [:faq]
12
+ # end
13
+ #
14
+ # @example Dynamic skills
15
+ # class WorkspaceAgent < RubyLLM::Agent
16
+ # inputs :workspace
17
+ # skills { [workspace.skill_collection] }
18
+ # end
19
+ #
20
+ module AgentExtensions
21
+ REQUIRED_AGENT_SINGLETON_METHODS = %i[apply_configuration runtime_context].freeze
22
+
23
+ module ClassMethods
24
+ include SourceDetection
25
+
26
+ def self.extended(base)
27
+ base.instance_variable_set(:@skill_sources, nil)
28
+ base.instance_variable_set(:@skill_only, nil)
29
+ end
30
+
31
+ def inherited(subclass)
32
+ super
33
+ subclass.instance_variable_set(:@skill_sources, @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup)
34
+ subclass.instance_variable_set(:@skill_only, @skill_only&.dup)
35
+ end
36
+
37
+ # Declare skill sources for this agent class.
38
+ #
39
+ # Called with no arguments, returns the current configuration.
40
+ # Called with sources or a block, sets the configuration.
41
+ #
42
+ # @param sources [Array] skill sources
43
+ # @param only [Array<Symbol, String>, nil] include only these skills
44
+ # @return [Hash] current configuration when called as a getter
45
+ def skills(*sources, only: nil, &block)
46
+ if sources.empty? && only.nil? && !block_given?
47
+ return {
48
+ sources: @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup,
49
+ only: @skill_only&.dup
50
+ }
51
+ end
52
+
53
+ @skill_sources = block_given? ? block : normalize_skill_sources(sources)
54
+ @skill_only = only&.dup
55
+ end
56
+
57
+ private
58
+
59
+ def normalize_skill_sources(raw_sources)
60
+ flatten_skill_sources(raw_sources).compact
61
+ end
62
+
63
+ def flatten_skill_sources(source)
64
+ return [] if source.nil?
65
+ return [source] if source.is_a?(String)
66
+ return [source] if loader_source?(source)
67
+ return source.empty? ? [] : [source] if database_collection_source?(source)
68
+ return source.flat_map { |item| flatten_skill_sources(item) } if source.is_a?(Array)
69
+
70
+ [source]
71
+ end
72
+ end
73
+
74
+ module InstanceMethods
75
+ # Add skills to this agent instance at runtime.
76
+ #
77
+ # @param sources [Array] skill sources
78
+ # @param only [Array<Symbol, String>, nil] include only these skills
79
+ # @return [self] for chaining
80
+ def with_skills(*sources, only: nil)
81
+ chat.with_skills(*sources, only: only)
82
+ self
83
+ end
84
+ end
85
+
86
+ module ConfigurationPatch
87
+ # RubyLLM 2.0 passes either a RubyLLM::Chat or an acts_as_chat record.
88
+ # Both respond to #with_skills (ChatExtensions / ActiveRecordExtensions),
89
+ # so skills apply directly to whatever the agent configured.
90
+ #
91
+ # apply_configuration is a :nodoc: hook; **options forwards any keyword a
92
+ # later 2.x release adds so the patch fails at super, not before it.
93
+ def apply_configuration(chat, input_values: {}, **options)
94
+ super
95
+ runtime = runtime_context(chat: chat, inputs: input_values)
96
+ apply_skills(chat, runtime)
97
+ end
98
+
99
+ private
100
+
101
+ def apply_skills(chat, runtime)
102
+ config = skills
103
+ sources = config[:sources]
104
+ return if sources.nil?
105
+
106
+ resolved_sources = if sources.is_a?(Proc)
107
+ runtime.instance_exec(&sources)
108
+ else
109
+ sources
110
+ end
111
+
112
+ normalized_sources = normalize_skill_sources(resolved_sources)
113
+ return if normalized_sources.empty?
114
+
115
+ validate_skill_sources!(normalized_sources)
116
+ chat.with_skills(*normalized_sources, only: config[:only])
117
+ end
118
+
119
+ def validate_skill_sources!(sources)
120
+ invalid_sources = sources.reject { |source| valid_skill_source?(source) }
121
+ return if invalid_sources.empty?
122
+
123
+ invalid_types = invalid_sources.map { |source| source.class.name || source.class.to_s }.uniq.join(", ")
124
+ raise ArgumentError,
125
+ "Invalid skill source(s): #{invalid_types}. Expected String path, Loader, or record collection."
126
+ end
127
+
128
+ def valid_skill_source?(source)
129
+ source.is_a?(String) || loader_source?(source) || database_collection_source?(source)
130
+ end
131
+ end
132
+
133
+ def self.included(base)
134
+ missing_methods = REQUIRED_AGENT_SINGLETON_METHODS.reject do |method_name|
135
+ base.singleton_class.private_method_defined?(method_name) || base.singleton_class.method_defined?(method_name)
136
+ end
137
+
138
+ if missing_methods.any?
139
+ raise LoadError,
140
+ "RubyLLM::Agent is missing required methods for ruby_llm-skills integration: #{missing_methods.join(", ")}"
141
+ end
142
+
143
+ base.extend(ClassMethods)
144
+ base.include(InstanceMethods)
145
+ base.singleton_class.prepend(ConfigurationPatch)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "source_detection"
3
4
  require_relative "skill_tool"
4
5
 
5
- module RubyLlm
6
+ module RubyLLM
6
7
  module Skills
7
8
  # Extensions for RubyLLM::Chat to enable skill integration.
8
9
  #
@@ -19,20 +20,22 @@ module RubyLlm
19
20
  # chat.with_skills(only: [:pdf_report])
20
21
  #
21
22
  module ChatExtensions
23
+ include SourceDetection
24
+
22
25
  # Add skills to this chat.
23
26
  #
24
27
  # @param sources [Array] skill sources - auto-detects type (directory, zip, collection)
25
28
  # @param only [Array<Symbol, String>, nil] include only these skills
26
29
  # @return [self] for chaining
27
30
  def with_skills(*sources, only: nil)
28
- sources = [RubyLlm::Skills.default_path] if sources.empty?
31
+ sources = [RubyLLM::Skills.default_path] if sources.empty?
29
32
  loaders = sources.map { |s| to_loader(s) }
30
33
 
31
- loader = (loaders.length == 1) ? loaders.first : RubyLlm::Skills.compose(*loaders)
34
+ loader = (loaders.length == 1) ? loaders.first : RubyLLM::Skills.compose(*loaders)
32
35
  loader = FilteredLoader.new(loader, only) if only
33
36
 
34
- skill_tool = RubyLlm::Skills::SkillTool.new(loader)
35
- with_tool(skill_tool)
37
+ skill_tool = RubyLLM::Skills::SkillTool.new(loader)
38
+ with_tools(skill_tool)
36
39
  end
37
40
 
38
41
  private
@@ -40,11 +43,14 @@ module RubyLlm
40
43
  def to_loader(source)
41
44
  case source
42
45
  when String
43
- RubyLlm::Skills.from_directory(source)
44
- when ->(s) { s.respond_to?(:to_a) && s.first&.respond_to?(:name) && s.first.respond_to?(:content) }
45
- RubyLlm::Skills.from_database(source)
46
- else
46
+ RubyLLM::Skills.from_directory(source)
47
+ when ->(s) { database_collection_source?(s) }
48
+ RubyLLM::Skills.from_database(source)
49
+ when ->(s) { loader_source?(s) }
47
50
  source
51
+ else
52
+ raise ArgumentError,
53
+ "Invalid skill source: #{source.class}. Expected String path, Loader, or record collection."
48
54
  end
49
55
  end
50
56
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Combines multiple loaders into a single source.
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Loads skills from database records.
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Base error class for all skills-related errors.
6
6
  # Rescue this to catch any error from the gem.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Loads skills from a filesystem directory.
6
6
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Base class for skill loaders.
6
6
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- module RubyLlm
5
+ module RubyLLM
6
6
  module Skills
7
7
  # Parses SKILL.md files with YAML frontmatter.
8
8
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Rails integration for RubyLLM::Skills.
6
6
  #
@@ -9,14 +9,14 @@ module RubyLlm
9
9
  #
10
10
  class Railtie < ::Rails::Railtie
11
11
  initializer "ruby_llm_skills.configure" do
12
- RubyLlm::Skills.default_path = Rails.root.join("app", "skills").to_s
12
+ RubyLLM::Skills.default_path = Rails.root.join("app", "skills").to_s
13
13
  end
14
14
 
15
- # Add app/skills to autoload paths
16
- initializer "ruby_llm_skills.autoload_paths" do |app|
15
+ # Add app/skills to autoload paths (before initialization)
16
+ config.before_configuration do |app|
17
17
  skills_path = Rails.root.join("app", "skills")
18
18
  if skills_path.exist?
19
- app.config.autoload_paths << skills_path.to_s
19
+ app.config.autoload_paths += [skills_path.to_s]
20
20
  end
21
21
  end
22
22
 
@@ -24,7 +24,7 @@ module RubyLlm
24
24
  initializer "ruby_llm_skills.active_record" do
25
25
  ActiveSupport.on_load(:active_record) do
26
26
  if defined?(RubyLLM::ActiveRecord::ChatMethods)
27
- RubyLLM::ActiveRecord::ChatMethods.include(RubyLlm::Skills::ActiveRecordExtensions)
27
+ RubyLLM::ActiveRecord::ChatMethods.include(RubyLLM::Skills::ActiveRecordExtensions)
28
28
  end
29
29
  end
30
30
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Represents a single skill with its metadata and content.
6
6
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "ruby_llm"
4
4
 
5
- module RubyLlm
5
+ module RubyLLM
6
6
  module Skills
7
7
  # A RubyLLM Tool that enables progressive skill loading.
8
8
  #
@@ -17,8 +17,8 @@ module RubyLlm
17
17
  # 3. Resources (scripts, references) can be loaded separately as needed
18
18
  #
19
19
  # @example Basic usage
20
- # loader = RubyLlm::Skills.from_directory("app/skills")
21
- # skill_tool = RubyLlm::Skills::SkillTool.new(loader)
20
+ # loader = RubyLLM::Skills.from_directory("app/skills")
21
+ # skill_tool = RubyLLM::Skills::SkillTool.new(loader)
22
22
  #
23
23
  # chat.with_tools(skill_tool)
24
24
  # chat.ask("Help me generate a PDF report")
@@ -27,15 +27,22 @@ module RubyLlm
27
27
  #
28
28
  class SkillTool < RubyLLM::Tool
29
29
  description "Execute a skill within the main conversation."
30
- param :command, type: "string",
31
- desc: "The skill name (e.g., 'pdf' or 'write-poem')"
32
- param :arguments, type: "string", required: false,
33
- desc: "Arguments passed after the command (e.g., '/write-poem about robots' passes 'about robots')"
34
- param :resource, type: "string", required: false,
35
- desc: "Optional resource path to load (e.g., 'scripts/helper.rb', 'references/guide.md')"
30
+ parameter :command, type: "string",
31
+ description: "The skill name (e.g., 'pdf' or 'write-poem')"
32
+ parameter :arguments, type: "string", required: false,
33
+ description: "Arguments passed after the command (e.g., '/write-poem about robots' passes 'about robots')"
34
+ parameter :resource, type: "string", required: false,
35
+ description: "Optional resource path to load (e.g., 'scripts/helper.rb', 'references/guide.md')"
36
36
 
37
37
  attr_reader :loader
38
38
 
39
+ # Tool name for RubyLLM.
40
+ #
41
+ # @return [String] "skill"
42
+ def self.tool_name
43
+ "skill"
44
+ end
45
+
39
46
  # Initialize with a skill loader.
40
47
  #
41
48
  # @param loader [Loader] any loader (FilesystemLoader, ZipLoader, etc.)
@@ -43,13 +50,6 @@ module RubyLlm
43
50
  @loader = loader
44
51
  end
45
52
 
46
- # Tool name for RubyLLM.
47
- #
48
- # @return [String] "skill"
49
- def name
50
- "skill"
51
- end
52
-
53
53
  # Dynamic description including available skills.
54
54
  #
55
55
  # @return [String] tool description with embedded skill metadata
@@ -98,7 +98,7 @@ module RubyLlm
98
98
  {
99
99
  name: name,
100
100
  description: description,
101
- parameters: params_schema
101
+ parameters: parameters_schema
102
102
  }
103
103
  end
104
104
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Skills
5
+ # Shared duck-typing predicates for classifying skill sources.
6
+ # Used by both AgentExtensions and ChatExtensions.
7
+ module SourceDetection
8
+ private
9
+
10
+ def loader_source?(source)
11
+ source.respond_to?(:list) && source.respond_to?(:find)
12
+ end
13
+
14
+ def database_collection_source?(source)
15
+ return false unless source.respond_to?(:to_a)
16
+
17
+ # ActiveRecord relations/CollectionProxy: recognizable even when empty
18
+ return true if source.respond_to?(:klass) && source.respond_to?(:where_values_hash)
19
+
20
+ first = source.first
21
+ first&.respond_to?(:name) && first.respond_to?(:content)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
5
  # Validates skill structure according to the Agent Skills specification.
6
6
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLlm
3
+ module RubyLLM
4
4
  module Skills
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0.pre1"
6
6
  end
7
7
  end
@@ -10,14 +10,16 @@ require_relative "skills/skill"
10
10
  require_relative "skills/loader"
11
11
  require_relative "skills/filesystem_loader"
12
12
  require_relative "skills/chat_extensions"
13
+ require_relative "skills/agent_extensions"
13
14
 
14
15
  # Load Rails integration when Rails is available
15
16
  require_relative "skills/railtie" if defined?(Rails::Railtie)
16
17
 
17
18
  # Extend RubyLLM::Chat with skill methods
18
- RubyLLM::Chat.include(RubyLlm::Skills::ChatExtensions)
19
+ RubyLLM::Chat.include(RubyLLM::Skills::ChatExtensions)
20
+ RubyLLM::Agent.include(RubyLLM::Skills::AgentExtensions)
19
21
 
20
- module RubyLlm
22
+ module RubyLLM
21
23
  module Skills
22
24
  class << self
23
25
  attr_accessor :default_path
@@ -27,7 +29,7 @@ module RubyLlm
27
29
  # @param path [String] path to skills directory (defaults to default_path)
28
30
  # @return [FilesystemLoader] loader for the directory
29
31
  # @example
30
- # RubyLlm::Skills.from_directory("app/skills")
32
+ # RubyLLM::Skills.from_directory("app/skills")
31
33
  def from_directory(path = default_path)
32
34
  FilesystemLoader.new(path)
33
35
  end
@@ -38,7 +40,7 @@ module RubyLlm
38
40
  # @return [Skill] the loaded skill
39
41
  # @raise [LoadError] if SKILL.md not found
40
42
  # @example
41
- # RubyLlm::Skills.load("app/skills/my-skill")
43
+ # RubyLLM::Skills.load("app/skills/my-skill")
42
44
  def load(path)
43
45
  skill_md = File.join(path, "SKILL.md")
44
46
  raise LoadError, "SKILL.md not found in #{path}" unless File.exist?(skill_md)
@@ -52,7 +54,7 @@ module RubyLlm
52
54
  # @param records [ActiveRecord::Relation, Array] collection of skill records
53
55
  # @return [DatabaseLoader] loader for the records
54
56
  # @example
55
- # RubyLlm::Skills.from_database(Skill.where(active: true))
57
+ # RubyLLM::Skills.from_database(Skill.where(active: true))
56
58
  def from_database(records)
57
59
  require_relative "skills/database_loader"
58
60
  DatabaseLoader.new(records)
@@ -63,9 +65,9 @@ module RubyLlm
63
65
  # @param loaders [Array<Loader>] loaders to combine
64
66
  # @return [CompositeLoader] combined loader
65
67
  # @example
66
- # RubyLlm::Skills.compose(
67
- # RubyLlm::Skills.from_directory("app/skills"),
68
- # RubyLlm::Skills.from_database(Skill.all)
68
+ # RubyLLM::Skills.compose(
69
+ # RubyLLM::Skills.from_directory("app/skills"),
70
+ # RubyLLM::Skills.from_database(Skill.all)
69
71
  # )
70
72
  def compose(*loaders)
71
73
  require_relative "skills/composite_loader"
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-skills
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kieran Klaassen
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2026-01-16 00:00:00.000000000 Z
11
+ date: 2026-09-10 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: ruby_llm
@@ -15,14 +16,20 @@ dependencies:
15
16
  requirements:
16
17
  - - ">="
17
18
  - !ruby/object:Gem::Version
18
- version: '1.10'
19
+ version: 2.0.0.rc2
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
19
23
  type: :runtime
20
24
  prerelease: false
21
25
  version_requirements: !ruby/object:Gem::Requirement
22
26
  requirements:
23
27
  - - ">="
24
28
  - !ruby/object:Gem::Version
25
- version: '1.10'
29
+ version: 2.0.0.rc2
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
26
33
  description: Load, validate, and integrate Agent Skills with RubyLLM. Supports the
27
34
  open Agent Skills specification for progressive skill discovery and loading from
28
35
  filesystem, zip archives, and databases.
@@ -35,9 +42,11 @@ files:
35
42
  - CHANGELOG.md
36
43
  - LICENSE.txt
37
44
  - README.md
45
+ - lib/generators/skill/USAGE
38
46
  - lib/generators/skill/skill_generator.rb
39
47
  - lib/generators/skill/templates/SKILL.md.tt
40
48
  - lib/ruby_llm/skills.rb
49
+ - lib/ruby_llm/skills/agent_extensions.rb
41
50
  - lib/ruby_llm/skills/chat_extensions.rb
42
51
  - lib/ruby_llm/skills/composite_loader.rb
43
52
  - lib/ruby_llm/skills/database_loader.rb
@@ -48,6 +57,7 @@ files:
48
57
  - lib/ruby_llm/skills/railtie.rb
49
58
  - lib/ruby_llm/skills/skill.rb
50
59
  - lib/ruby_llm/skills/skill_tool.rb
60
+ - lib/ruby_llm/skills/source_detection.rb
51
61
  - lib/ruby_llm/skills/tasks/skills.rake
52
62
  - lib/ruby_llm/skills/validator.rb
53
63
  - lib/ruby_llm/skills/version.rb
@@ -59,6 +69,7 @@ metadata:
59
69
  source_code_uri: https://github.com/kieranklaassen/ruby_llm-skills
60
70
  changelog_uri: https://github.com/kieranklaassen/ruby_llm-skills/blob/main/CHANGELOG.md
61
71
  rubygems_mfa_required: 'true'
72
+ post_install_message:
62
73
  rdoc_options: []
63
74
  require_paths:
64
75
  - lib
@@ -69,11 +80,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
69
80
  version: 3.2.0
70
81
  required_rubygems_version: !ruby/object:Gem::Requirement
71
82
  requirements:
72
- - - ">="
83
+ - - ">"
73
84
  - !ruby/object:Gem::Version
74
- version: '0'
85
+ version: 1.3.1
75
86
  requirements: []
76
- rubygems_version: 3.6.2
87
+ rubygems_version: 3.4.10
88
+ signing_key:
77
89
  specification_version: 4
78
90
  summary: Agent Skills extension for RubyLLM
79
91
  test_files: []