gem-skill 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0d438a4ff4e7229ac130284f672c464bbaf770f04ba640177f0effce8822f9ec
4
+ data.tar.gz: 7f86cbc1951664f5ba80602238db6d671450adb3bf7776ec2be0323df342de05
5
+ SHA512:
6
+ metadata.gz: c120f84db49f729f342a6640375cbd12b9fceb525645406b64488d209973ca77e09d111ed6c92ae9ec9c4107ea060cf32ce4b1726ccb9064fd78b5223b88995d
7
+ data.tar.gz: a6747e0d10b773bfdd3ee7b9c562da01e061cd2fb40341c8c99e41c56be931dff27642d738f4f8e49afff85efa3fec1242ed2f6de036ab9a2880677be598b3de
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-16
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Dewayne VanHoozer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # gem-skill
2
+
3
+ Generates Claude Code skill files from Ruby gem documentation and caches them
4
+ globally so every project that uses a gem can share the same pre-built knowledge.
5
+
6
+ ## The problem it solves
7
+
8
+ Every time Claude Code encounters a gem it hasn't seen in the current context, it
9
+ re-reads the README, scans examples, and figures out the API. That costs tokens
10
+ and time — and the result evaporates when the conversation ends.
11
+
12
+ `gem-skill` runs that pipeline once, offline, and stores the output as a
13
+ `SKILL.md` in `~/.gem/skills`. Projects symlink to the cached version, so Claude
14
+ has accurate, version-specific knowledge about each gem without repeating the
15
+ ingestion work.
16
+
17
+ ## How the cache is laid out
18
+
19
+ ```
20
+ ~/.gem/skills/
21
+ └── chunker-ruby/
22
+ ├── 1.2.3/
23
+ │ ├── SKILL.md ← generated skill
24
+ │ └── metadata.json ← gem name, version, model used, generated_at
25
+ └── 1.4.0/
26
+ ├── SKILL.md
27
+ └── metadata.json
28
+ ```
29
+
30
+ Each project's `.claude/skills/` holds symlinks that point into this cache:
31
+
32
+ ```
33
+ your-app/.claude/skills/
34
+ └── chunker-ruby.md → ~/.gem/skills/chunker-ruby/1.2.3/SKILL.md
35
+ ```
36
+
37
+ Two projects that pin different versions of the same gem each get the right
38
+ skill; the underlying content is generated once and shared.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ gem install gem-skill
44
+ ```
45
+
46
+ This gives you the `gem skill` subcommand.
47
+
48
+ For `bundle skill` support (project-aware, reads `Gemfile.lock`), also run:
49
+
50
+ ```bash
51
+ bundle plugin install gem-skill
52
+ ```
53
+
54
+ or add it to your `Gemfile`:
55
+
56
+ ```ruby
57
+ plugin "gem-skill"
58
+ ```
59
+
60
+ ## Requirements
61
+
62
+ `gem-skill` uses [RubyLLM](https://github.com/crmne/ruby_llm) to generate
63
+ skills. Configure at least one provider API key before running:
64
+
65
+ ```bash
66
+ export OPENAI_API_KEY="..." # default model: gpt-5.5
67
+ export ANTHROPIC_API_KEY="..." # or use Claude
68
+ export GEMINI_API_KEY="..." # or Gemini
69
+ ```
70
+
71
+ ## Configuration
72
+
73
+ Two environment variables control `gem-skill`'s behaviour:
74
+
75
+ | Variable | Default | Description |
76
+ |---|---|---|
77
+ | `GEMSKILL_DIR` | `~/.gem/skills` | Root directory for the skill cache |
78
+ | `GEMSKILL_MODEL` | `gpt-5.5` | LLM model used when generating skills |
79
+
80
+ ```bash
81
+ # Store skills on a shared drive accessible to all projects
82
+ export GEMSKILL_DIR="/Volumes/shared/gem-skills"
83
+
84
+ # Switch the default model to Claude
85
+ export GEMSKILL_MODEL="claude-sonnet-4-6"
86
+ ```
87
+
88
+ The `--model` flag on any command overrides `GEMSKILL_MODEL` for that
89
+ invocation. `GEMSKILL_DIR` applies everywhere the cache is read or written.
90
+
91
+ ## Usage
92
+
93
+ ### `gem skill` — global cache management
94
+
95
+ ```bash
96
+ # Generate a skill for an installed gem (version auto-detected)
97
+ gem skill install chunker-ruby
98
+
99
+ # Install skills for multiple gems at once (runs concurrently)
100
+ gem skill install chunker-ruby faraday debug_me
101
+
102
+ # Force regeneration even if already cached
103
+ gem skill install chunker-ruby --force
104
+
105
+ # Use a different model
106
+ gem skill install chunker-ruby --model claude-haiku-4-5
107
+
108
+ # Show everything in the cache
109
+ gem skill list
110
+
111
+ # Remove a specific cached version
112
+ gem skill purge chunker-ruby 1.2.3
113
+
114
+ # Remove all cached versions of a gem
115
+ gem skill purge chunker-ruby --all
116
+ ```
117
+
118
+ If a gem isn't installed locally, `gem skill install` will install it first.
119
+
120
+ ### `gem install --with-skill`
121
+
122
+ Generate skills for gems as you install them:
123
+
124
+ ```bash
125
+ gem install faraday debug_me --with-skill
126
+ ```
127
+
128
+ Skills are generated concurrently after all gems finish installing.
129
+
130
+ ### `bundle skill` — project-aware, driven by Gemfile.lock
131
+
132
+ Run from your project root after `bundle install`:
133
+
134
+ ```bash
135
+ # Generate and link skills for all direct dependencies
136
+ bundle skill install
137
+
138
+ # Re-sync after bundle update (skips gems already at the correct version)
139
+ bundle skill refresh
140
+
141
+ # Show what's linked in this project
142
+ bundle skill list
143
+
144
+ # Options available on install and refresh
145
+ bundle skill install --force
146
+ bundle skill install --model claude-haiku-4-5
147
+ ```
148
+
149
+ The `install` and `refresh` commands stream LLM output as it is generated, so
150
+ you see progress rather than a silent wait.
151
+
152
+ ## What gets generated
153
+
154
+ Each `SKILL.md` covers:
155
+
156
+ - **Overview** — what the gem does and when to use it
157
+ - **Installation** — exact Gemfile lines and post-install steps
158
+ - **Core API** — key classes and methods with real code examples
159
+ - **Common Patterns** — the 3–5 most frequent real-world usage patterns
160
+ - **Gotchas & Edge Cases** — surprising defaults, version-specific behavior,
161
+ thread safety, encoding issues
162
+ - **Configuration** — initializer patterns and environment variables
163
+ - **Testing** — how to test code that uses the gem
164
+
165
+ The content is synthesized from three sources, tried in priority order:
166
+
167
+ 1. Local gem install (`Gem::Specification` → `gem_dir`) — README and CHANGELOG
168
+ 2. RubyGems API — summary, runtime dependencies, source URI
169
+ 3. GitHub raw README — fetched when the gem isn't installed locally
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ git clone https://github.com/madbomber/gem-skill
175
+ cd gem-skill
176
+ bundle install
177
+ bundle exec rake test
178
+ ```
179
+
180
+ ## Contributing
181
+
182
+ Bug reports and pull requests welcome at https://github.com/madbomber/gem-skill.
183
+
184
+ ## License
185
+
186
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+
7
+ module Gem::Skill
8
+ # Manages the global ~/.gem/skills cache.
9
+ # Structure: ~/.gem/skills/<gem_name>/<version>/SKILL.md
10
+ module Cache
11
+ ROOT = File.expand_path(ENV.fetch("GEMSKILL_DIR", "~/.gem/skills")).freeze
12
+
13
+ def self.root = ROOT
14
+
15
+ def self.skill_path(gem_name, version)
16
+ File.join(ROOT, gem_name, version, "SKILL.md")
17
+ end
18
+
19
+ def self.metadata_path(gem_name, version)
20
+ File.join(ROOT, gem_name, version, "metadata.json")
21
+ end
22
+
23
+ def self.cached?(gem_name, version)
24
+ File.exist?(skill_path(gem_name, version))
25
+ end
26
+
27
+ def self.store(gem_name, version, skill_content, metadata = {})
28
+ dir = File.join(ROOT, gem_name, version)
29
+ FileUtils.mkdir_p(dir)
30
+ File.write(skill_path(gem_name, version), skill_content)
31
+ File.write(metadata_path(gem_name, version), JSON.generate(metadata.merge(
32
+ gem_name: gem_name,
33
+ version: version,
34
+ generated_at: Time.now.iso8601
35
+ )))
36
+ end
37
+
38
+ def self.read(gem_name, version)
39
+ path = skill_path(gem_name, version)
40
+ raise Error, "No cached skill for #{gem_name} #{version}" unless File.exist?(path)
41
+
42
+ File.read(path)
43
+ end
44
+
45
+ def self.versions(gem_name)
46
+ dir = File.join(ROOT, gem_name)
47
+ return [] unless Dir.exist?(dir)
48
+
49
+ Dir.children(dir).sort
50
+ end
51
+
52
+ def self.all_gems
53
+ return [] unless Dir.exist?(ROOT)
54
+
55
+ Dir.children(ROOT).sort
56
+ end
57
+
58
+ def self.purge(gem_name, version)
59
+ dir = File.join(ROOT, gem_name, version)
60
+ FileUtils.rm_rf(dir)
61
+ parent = File.join(ROOT, gem_name)
62
+ Dir.rmdir(parent) if Dir.exist?(parent) && Dir.empty?(parent)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "gem/skill"
6
+
7
+ module Gem::Skill
8
+ # Handles `bundle skill SUBCOMMAND` via Bundler's plugin API (plugins.rb).
9
+ # Project-aware: reads Gemfile.lock and manages .claude/skills/ symlinks.
10
+ module BundlerCommand
11
+ SUBCOMMANDS = %w[install refresh list].freeze
12
+
13
+ def self.run(args)
14
+ Gem::Skill.configure_llm!
15
+ opts, rest = parse_options(args)
16
+ subcmd = rest.shift
17
+
18
+ case subcmd
19
+ when "install" then install(opts)
20
+ when "refresh" then refresh(opts)
21
+ when "list" then list
22
+ when nil, "help", "--help"
23
+ puts usage
24
+ else
25
+ warn "gem-skill: unknown subcommand #{subcmd.inspect}"
26
+ warn usage
27
+ exit 1
28
+ end
29
+ end
30
+
31
+ def self.install(opts = {})
32
+ gems = Lockfile.gems
33
+ if gems.empty?
34
+ puts "No gems found in Gemfile.lock."
35
+ return
36
+ end
37
+
38
+ force = opts[:force]
39
+ model = opts[:model] || Generator::DEFAULT_MODEL
40
+ errors = []
41
+
42
+ puts "Installing skills for #{gems.size} gem(s) (model: #{model})..."
43
+ puts ""
44
+
45
+ gems.each do |gem_name, version|
46
+ if Cache.cached?(gem_name, version) && !force
47
+ print " skip #{gem_name} #{version}"
48
+ else
49
+ print " gen #{gem_name} #{version} "
50
+ $stdout.flush
51
+ Generator.new(gem_name, version, model: model).generate(force: force) do |chunk|
52
+ print chunk
53
+ $stdout.flush
54
+ end
55
+ end
56
+
57
+ Linker.link(gem_name, version)
58
+ puts " ✓"
59
+ rescue Gem::Skill::Error => e
60
+ puts " ✗ #{e.message}"
61
+ errors << "#{gem_name} #{version}: #{e.message}"
62
+ end
63
+
64
+ Linker.prune_dead_links
65
+
66
+ puts ""
67
+ puts "Done. #{gems.size - errors.size}/#{gems.size} skill(s) linked into .claude/skills/"
68
+
69
+ if errors.any?
70
+ puts ""
71
+ puts "Errors:"
72
+ errors.each { |e| puts " #{e}" }
73
+ end
74
+ end
75
+
76
+ def self.refresh(opts = {})
77
+ gems = Lockfile.gems
78
+ linked = Linker.linked_gems.to_h { |e| [e[:gem_name], e[:version]] }
79
+ force = opts[:force]
80
+ model = opts[:model] || Generator::DEFAULT_MODEL
81
+ errors = []
82
+
83
+ puts "Refreshing skills (model: #{model})..."
84
+ puts ""
85
+
86
+ gems.each do |gem_name, version|
87
+ if !force && linked[gem_name] == version
88
+ puts " ok #{gem_name} #{version}"
89
+ next
90
+ end
91
+
92
+ action = linked.key?(gem_name) ? "update" : "new"
93
+ print " #{action.ljust(6)}#{gem_name} #{version} "
94
+ $stdout.flush
95
+
96
+ Generator.new(gem_name, version, model: model).generate(force: force) do |chunk|
97
+ print chunk
98
+ $stdout.flush
99
+ end
100
+
101
+ Linker.link(gem_name, version)
102
+ puts " ✓"
103
+ rescue Gem::Skill::Error => e
104
+ puts " ✗ #{e.message}"
105
+ errors << "#{gem_name} #{version}: #{e.message}"
106
+ end
107
+
108
+ Linker.prune_dead_links
109
+
110
+ puts ""
111
+ puts "Refreshed."
112
+ if errors.any?
113
+ puts ""
114
+ puts "Errors:"
115
+ errors.each { |e| puts " #{e}" }
116
+ end
117
+ end
118
+
119
+ def self.list
120
+ entries = Linker.linked_gems
121
+ if entries.empty?
122
+ puts "No skills linked in this project."
123
+ puts "Run: bundle skill install"
124
+ return
125
+ end
126
+
127
+ ok = entries.count { |e| e[:valid] }
128
+ broken = entries.size - ok
129
+
130
+ puts "Skills linked in .claude/skills/ (#{ok} ok#{broken > 0 ? ", #{broken} broken" : ""}):"
131
+ puts ""
132
+ entries.each do |e|
133
+ status = e[:valid] ? "ok " : "BROKEN"
134
+ puts " [#{status}] %-30s %s" % [e[:gem_name], e[:version]]
135
+ end
136
+ end
137
+
138
+ # --- private ---
139
+
140
+ def self.parse_options(args)
141
+ opts = {}
142
+ remaining = []
143
+
144
+ args.each do |arg|
145
+ case arg
146
+ when "--force" then opts[:force] = true
147
+ when /\A--model(?:=(.+))?\z/
148
+ opts[:model] = $1 || args[args.index(arg) + 1]
149
+ else
150
+ remaining << arg unless opts[:model].nil? && arg !~ /\A--/
151
+ remaining << arg if arg !~ /\A--/
152
+ end
153
+ end
154
+
155
+ [opts, remaining]
156
+ end
157
+ private_class_method :parse_options
158
+
159
+ def self.usage
160
+ <<~USAGE
161
+ Usage: bundle skill SUBCOMMAND [OPTIONS]
162
+
163
+ Subcommands:
164
+ install Generate and link skills for all gems in Gemfile.lock
165
+ refresh Re-sync .claude/skills/ after bundle update
166
+ list Show skills linked in this project
167
+
168
+ Options:
169
+ --force Regenerate even if already cached
170
+ --model MODEL LLM model to use (default: #{Generator::DEFAULT_MODEL})
171
+ USAGE
172
+ end
173
+ private_class_method :usage
174
+ end
175
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/command"
4
+ require "fileutils"
5
+ require "json"
6
+ require "tty-spinner"
7
+ require "gem/skill"
8
+
9
+ # Registered as `gem skill` via lib/rubygems_plugin.rb.
10
+ # Manages the global ~/.gem/skills cache.
11
+ class Gem::Commands::SkillCommand < Gem::Command
12
+ def initialize
13
+ super "skill", "Manage Claude Code AI skills for Ruby gems"
14
+
15
+ add_option("-f", "--force", "Regenerate even if already cached") { |_, o| o[:force] = true }
16
+ add_option("-a", "--all", "Purge all cached versions of a gem") { |_, o| o[:all] = true }
17
+ add_option("-m", "--model MODEL", "LLM model to use (default: #{Gem::Skill::Generator::DEFAULT_MODEL})") do |model, o|
18
+ o[:model] = model
19
+ end
20
+ end
21
+
22
+ def arguments
23
+ "SUBCOMMAND one of: install, list, purge"
24
+ end
25
+
26
+ def usage
27
+ "#{program_name} install GEM_NAME [GEM_NAME ...]\n" \
28
+ " #{program_name} list\n" \
29
+ " #{program_name} purge GEM_NAME VERSION\n" \
30
+ " #{program_name} purge GEM_NAME --all"
31
+ end
32
+
33
+ def description
34
+ <<~DESC
35
+ install Generate and cache a SKILL.md for a gem.
36
+ list Show all skills in the global cache (~/.gem/skills).
37
+ purge Remove a specific cached version.
38
+
39
+ Use 'bundle skill install' (after: bundle plugin install gem-skill)
40
+ to generate and link skills for an entire project from Gemfile.lock.
41
+ DESC
42
+ end
43
+
44
+ def execute
45
+ Gem::Skill.configure_llm!
46
+ subcmd = options[:args].shift
47
+ case subcmd
48
+ when "install" then cmd_install
49
+ when "list" then cmd_list
50
+ when "purge" then cmd_purge
51
+ when nil
52
+ say usage
53
+ else
54
+ alert_error "Unknown subcommand: #{subcmd.inspect}"
55
+ say usage
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def cmd_install
62
+ gem_names = options[:args].dup
63
+ options[:args].clear
64
+
65
+ if gem_names.empty?
66
+ alert_error "gem_name required. Usage: gem skill install GEM_NAME [GEM_NAME ...]"
67
+ return
68
+ end
69
+
70
+ force = options[:force]
71
+ model = options[:model] || Gem::Skill::Generator::DEFAULT_MODEL
72
+
73
+ multi = TTY::Spinner::Multi.new(
74
+ "[:spinner] Generating skills (#{model})",
75
+ format: :dots,
76
+ output: $stderr
77
+ )
78
+
79
+ threads = gem_names.map do |gem_name|
80
+ spinner = multi.register(" [:spinner] :title")
81
+ spinner.update(title: gem_name)
82
+ Thread.new(gem_name, spinner) { |name, sp| install_one(name, spinner: sp, force: force, model: model) }
83
+ end
84
+ threads.each(&:join)
85
+
86
+ say "Tip: run 'bundle plugin install gem-skill' to enable 'bundle skill'."
87
+ end
88
+
89
+ def install_one(gem_name, spinner:, force:, model:)
90
+ spinner.auto_spin
91
+
92
+ version = resolve_installed_version(gem_name)
93
+ if version.nil?
94
+ spinner.update(title: "#{gem_name} (installing...)")
95
+ version = install_gem(gem_name)
96
+ end
97
+
98
+ if Gem::Skill::Cache.cached?(gem_name, version) && !force
99
+ spinner.update(title: "#{gem_name} #{version}")
100
+ spinner.success("already cached")
101
+ return
102
+ end
103
+
104
+ spinner.update(title: "#{gem_name} #{version}")
105
+ Gem::Skill::Generator.new(gem_name, version, model: model).generate(force: force)
106
+ spinner.success("done")
107
+ rescue => e
108
+ spinner.error("#{gem_name} failed")
109
+ alert_error "#{gem_name}: #{e.message}"
110
+ end
111
+
112
+ def cmd_list
113
+ gems = Gem::Skill::Cache.all_gems
114
+ if gems.empty?
115
+ say "No skills cached yet."
116
+ say "Run: gem skill install GEM_NAME"
117
+ return
118
+ end
119
+
120
+ say "Cached skills in #{Gem::Skill::Cache.root}:"
121
+ say ""
122
+ gems.each do |name|
123
+ versions = Gem::Skill::Cache.versions(name)
124
+ say " %-30s %s" % [name, versions.join(", ")]
125
+ end
126
+ say ""
127
+ say "#{gems.size} gem(s), #{gems.sum { |n| Gem::Skill::Cache.versions(n).size }} version(s) total."
128
+ end
129
+
130
+ def cmd_purge
131
+ gem_name = options[:args].shift
132
+ unless gem_name
133
+ alert_error "Usage: gem skill purge GEM_NAME VERSION\n gem skill purge GEM_NAME --all"
134
+ return
135
+ end
136
+
137
+ if options[:all]
138
+ versions = Gem::Skill::Cache.versions(gem_name)
139
+ if versions.empty?
140
+ alert_error "No cached versions for '#{gem_name}'"
141
+ return
142
+ end
143
+ versions.each { |v| Gem::Skill::Cache.purge(gem_name, v) }
144
+ say "Purged #{versions.size} version(s) of #{gem_name}"
145
+ return
146
+ end
147
+
148
+ version = options[:args].shift
149
+ unless version
150
+ alert_error "Usage: gem skill purge GEM_NAME VERSION\n gem skill purge GEM_NAME --all"
151
+ return
152
+ end
153
+
154
+ unless Gem::Skill::Cache.cached?(gem_name, version)
155
+ alert_error "Not cached: #{gem_name} #{version}"
156
+ return
157
+ end
158
+
159
+ Gem::Skill::Cache.purge(gem_name, version)
160
+ say "Purged: #{gem_name} #{version}"
161
+ end
162
+
163
+ def resolve_installed_version(gem_name)
164
+ Gem::Specification.find_by_name(gem_name)&.version&.to_s
165
+ rescue Gem::MissingSpecError
166
+ nil
167
+ end
168
+
169
+ def install_gem(gem_name, version = nil)
170
+ req = version ? Gem::Requirement.new("= #{version}") : Gem::Requirement.default
171
+ specs = Gem.install(gem_name, req)
172
+ specs.find { |s| s.name == gem_name }&.version&.to_s
173
+ rescue Gem::InstallError, Gem::GemNotFoundException, StandardError => e
174
+ raise Gem::Skill::Error, "Could not install '#{gem_name}': #{e.message}"
175
+ end
176
+ end