gem-skill 0.2.0 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +63 -0
- data/README.md +13 -1
- data/docs/commands/bundle-skill.md +2 -0
- data/docs/commands/gem-skill.md +8 -0
- data/docs/configuration.md +24 -0
- data/lib/gem/skill/cli/bundle_command.rb +26 -19
- data/lib/gem/skill/cli/gem_command.rb +19 -7
- data/lib/gem/skill/generator.rb +45 -13
- data/lib/gem/skill/runner.rb +8 -2
- data/lib/gem/skill/verifier.rb +4 -1
- data/lib/gem/skill/version.rb +1 -1
- data/lib/gem/skill.rb +24 -1
- metadata +34 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc3ecc8f770110cc9f8605e149ff2d78c44a388a76add0046c595b01ecbf882
|
|
4
|
+
data.tar.gz: 21ac18bb7bcc6de52ac9987561da30821be843cca322f0b45b8e04ed1a9af3a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f6fecb42f713486c2abddd7041c6a7294ed4e0cf07affd97457739fe0bd59c8fb080ee80b672817e9a182ff8359896235f2c2c56f16a54eca9528290a3fd07d
|
|
7
|
+
data.tar.gz: acd79346e7b20fcead6bfff93f4f21aa58f81ac203b3fec5616f2f5f55b6d4337c0919cd01a1790159c7e256ec579c14d8e25c2aaa4cd5d85479fea826c4e3fa
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
- Bundled `ruby-gem-skills` "router" skill (shipped at the gem root) that teaches assistants how to find cached gem skills in `~/.gem/skills` — a directory neither Claude Code nor Codex scans by default. `gem skill setup` now also copies this skill into each detected assistant's default root (`~/.claude/skills`, `~/.codex/skills`, `~/.agents/skills`), creating none that don't already exist. `Gem::Skill::ROUTER_SKILL_NAME` / `ROUTER_SKILL_DIR` locate the bundled template.
|
|
10
|
+
- `GEMSKIL_MAX_TOKENS` environment variable (default `32767`) — caps the number of output tokens the LLM may generate per skill file. Raise this if generated `SKILL.md` files are being truncated.
|
|
11
|
+
- `--max-tokens TOKENS` CLI flag on `gem skill install`, `bundle skill install`, and `bundle skill refresh` — overrides `GEMSKIL_MAX_TOKENS` for a single invocation. Provider-correct parameter name is selected automatically (`max_completion_tokens` for OpenAI models, `max_tokens` for all others).
|
|
12
|
+
- `GEMSKILL_TEMPERATURE` environment variable (default `0.2`) — sampling temperature for generation. Lower values produce more consistent, less variable output, appropriate for factual reference docs. Applied only to models that support a temperature parameter; reasoning models (e.g. `gpt-5.5`) report `temperature: false` in their RubyLLM metadata and the value is silently skipped for them.
|
|
13
|
+
- `--temperature TEMP` CLI flag on `gem skill install` — overrides `GEMSKILL_TEMPERATURE` for a single invocation.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `strip_wrapper_fence` no longer deletes the closing ``` of a skill that legitimately ends with a code block. The trailing wrapper fence is now stripped only when a matching opening wrapper fence (```` ```markdown ```` / ```` ``` ```` at the very start) was present. Previously the final ``` was removed unconditionally, leaving the last code block open at end-of-file — which looked like the LLM truncating its output but was entirely a post-processing bug. Regenerate affected skills with `gem skill install GEM --force`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `Generator::MAX_TOKENS` constant reads from `GEMSKIL_MAX_TOKENS` and is passed via `with_params` to every `RubyLLM.chat` call, covering both streaming and non-streaming paths.
|
|
20
|
+
- `Generator#initialize` now accepts `max_tokens:` and `temperature:` keywords so callers can override the token cap and sampling temperature per-instance without touching the environment.
|
|
21
|
+
- `Generator` sets `with_temperature` only when the model's RubyLLM metadata does not mark temperature unsupported, avoiding API errors on reasoning models.
|
|
22
|
+
- `Runner.install_skill` accepts and forwards `max_tokens:` and `temperature:` to `Generator.new`.
|
|
23
|
+
- `README.md` Configuration section now lists all environment variables (gem-skill-specific and provider API keys) in a single reference table.
|
|
24
|
+
- `docs/configuration.md` documents the new `GEMSKIL_MAX_TOKENS` and `GEMSKILL_TEMPERATURE` variables alongside the existing `GEMSKILL_MODEL` entry.
|
|
25
|
+
- `docs/commands/gem-skill.md` and `docs/commands/bundle-skill.md` option tables updated with `--max-tokens` and `--temperature`.
|
|
10
26
|
|
|
11
27
|
## [0.2.0] - 2026-06-19
|
|
12
28
|
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this gem does
|
|
6
|
+
|
|
7
|
+
`gem-skill` generates `SKILL.md` files from Ruby gem documentation using an LLM (via RubyLLM), caches them globally at `~/.gem/skills/<gem_name>/<version>/SKILL.md`, and symlinks them into projects as directory links: `.claude/skills/<gem_name>/ → ~/.gem/skills/<gem_name>/<version>/`. It registers two CLI entry points: `gem skill` (global cache management) and `bundle skill` (project-aware, driven by `Gemfile.lock`), plus a `gem install --with-skill` flag.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle install
|
|
13
|
+
bundle exec rake test # run all tests (default task)
|
|
14
|
+
ruby -Ilib:test test/gem/skill/cache_test.rb # run a single test file
|
|
15
|
+
bundle exec ruby scripts/e2e_test [GEM VERSION MODEL] # live LLM end-to-end test (needs a provider API key)
|
|
16
|
+
bin/dev_install # point Bundler's plugin indexes at this source tree so
|
|
17
|
+
# `bundle skill` runs local code (no build/release needed)
|
|
18
|
+
bin/dev_install --reset # restore to the released gem install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Docs site is MkDocs (`mkdocs.yml`, `docs/`), deployed by `.github/workflows/deploy-github-pages.yml`.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
### Entry points
|
|
26
|
+
|
|
27
|
+
- `lib/rubygems_plugin.rb` — loaded automatically by RubyGems; registers `gem skill` (`Gem::Commands::SkillCommand`) AND prepends `--with-skill` onto `gem install`, queuing installed gems and generating their skills in threads via a `Gem.post_install` hook + `at_exit`
|
|
28
|
+
- `plugins.rb` — Bundler plugin entry point; registers `bundle skill` via `Gem::Skill::BundlerPlugin` → `Gem::Skill::BundlerCommand`
|
|
29
|
+
|
|
30
|
+
### Core pipeline (in call order)
|
|
31
|
+
|
|
32
|
+
1. **`Lockfile`** (`lib/gem/skill/lockfile.rb`) — parses `Gemfile.lock`; extracts direct-dependency name→version pairs by cross-referencing the `DEPENDENCIES` and `specs:` sections
|
|
33
|
+
2. **`Fetcher`** (`lib/gem/skill/fetcher.rb`) — collects raw docs for a single gem/version in priority order: (1) local `Gem::Specification` gem_dir, (2) RubyGems API JSON, (3) GitHub raw README. Returns `{metadata:, readme:, changelog:}` with only populated keys
|
|
34
|
+
3. **`Generator`** (`lib/gem/skill/generator.rb`) — formats fetched sources into a prompt, calls RubyLLM, strips any wrapping code fence, runs `Frontmatter.build`, stores result via `Cache`. Supports streaming via block
|
|
35
|
+
4. **`Frontmatter`** (`lib/gem/skill/frontmatter.rb`) — deterministically (no LLM) builds/replaces the YAML frontmatter (`name` + `description`) that makes a SKILL.md discoverable by both Claude Code and Codex; enforces both assistants' name/description constraints
|
|
36
|
+
5. **`Cache`** (`lib/gem/skill/cache.rb`) — reads/writes `~/.gem/skills/<name>/<version>/SKILL.md` and `metadata.json`; `merge_metadata` string-normalizes keys
|
|
37
|
+
6. **`Verifier`** (`lib/gem/skill/verifier.rb`) — optional second LLM pass (`--verify` or `gem skill verify`) that re-checks a generated skill against the gem's actual installed source and corrects wrong signatures/claims. `changed?` is decided by a deterministic content diff, never the model's self-report; unverifiable (no local source) is recorded in metadata, not an error
|
|
38
|
+
7. **`Linker`** (`lib/gem/skill/linker.rb`) — creates/updates directory symlinks in the project skills dir pointing into the cache; `prune_dead_links` removes broken symlinks after a refresh
|
|
39
|
+
|
|
40
|
+
**`Runner`** (`lib/gem/skill/runner.rb`) is the shared generate+cache+link+verify orchestration used by both CLI commands; it returns a `Runner::Result` (`Data.define`) and drives a TTY spinner passed in by the caller.
|
|
41
|
+
|
|
42
|
+
### CLI layer
|
|
43
|
+
|
|
44
|
+
- `lib/gem/skill/cli/gem_command.rb` — `Gem::Commands::SkillCommand`; subcommands: `install`, `verify`, `list`, `purge`, `setup`. Multi-gem installs run concurrently with the `async` gem (`Async::Barrier`). `setup` registers the Bundler plugin and copies the bundled router skill (`ruby-gem-skills/SKILL.md` at repo root) into each detected assistant's global skill root
|
|
45
|
+
- `lib/gem/skill/cli/bundle_command.rb` — `Gem::Skill::BundlerCommand`; subcommands: `install`, `refresh`, `list`. `install` = generate + link all direct lockfile gems; `refresh` = skip gems already linked at the correct version. Parses its own flags (`--force`, `--verify`, `--model`, `--max-tokens`, `--temperature`, `--version`)
|
|
46
|
+
|
|
47
|
+
### LLM configuration
|
|
48
|
+
|
|
49
|
+
`Gem::Skill.configure_llm!` reads provider API keys from environment variables (see `ENV_KEY_MAP` in `lib/gem/skill.rb`) and configures RubyLLM. Called automatically by both CLI entry points. No-op if already configured.
|
|
50
|
+
|
|
51
|
+
## Key constants & environment variables
|
|
52
|
+
|
|
53
|
+
- `Cache::ROOT` = `~/.gem/skills` (override: `GEMSKILL_DIR`)
|
|
54
|
+
- `Linker::DEFAULT_PROJECT_DIR` = `.claude/skills` (override: `GEMSKILL_PROJECT_DIR`, re-read on every call — e.g. `.agents` for Codex)
|
|
55
|
+
- `Generator::DEFAULT_MODEL` = `"gpt-5.5"` (override: `GEMSKILL_MODEL` or `--model`)
|
|
56
|
+
- `Generator::MAX_TOKENS` = 32,767 (override: `GEMSKIL_MAX_TOKENS` — note the single-L spelling — or `--max-tokens`)
|
|
57
|
+
- `Generator::DEFAULT_TEMPERATURE` = 0.2 (override: `GEMSKILL_TEMPERATURE` or `--temperature`; skipped for models that reject temperature)
|
|
58
|
+
- `Generator::MAX_SOURCE_CHARS` = 60,000 (README truncation guard)
|
|
59
|
+
- `Gem::Skill::EXIT_VERIFY_FIXED` = 2 — grep-style exit status when `--verify` applied fixes (0 = clean, 1 = error)
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
Uses Minitest (`Minitest::TestTask`). Unit tests live in `test/gem/skill/`; shared cache-sandboxing helpers in `test/support/cache_helpers.rb`. `scripts/e2e_test` runs a full live pipeline (fetch → generate → cache → link) against a real LLM and requires at least one provider API key.
|
data/README.md
CHANGED
|
@@ -104,13 +104,22 @@ export GEMINI_API_KEY="..." # or Gemini
|
|
|
104
104
|
|
|
105
105
|
## Configuration
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
All environment variables and their defaults:
|
|
108
108
|
|
|
109
109
|
| Variable | Default | Description |
|
|
110
110
|
|---|---|---|
|
|
111
111
|
| `GEMSKILL_DIR` | `~/.gem/skills` | Root directory for the global skill cache |
|
|
112
112
|
| `GEMSKILL_PROJECT_DIR` | `.claude/skills` | Project-relative directory where `bundle skill` writes symlinks |
|
|
113
113
|
| `GEMSKILL_MODEL` | `gpt-5.5` | LLM model used when generating skills |
|
|
114
|
+
| `GEMSKIL_MAX_TOKENS` | `32767` | Maximum output tokens per generated skill file |
|
|
115
|
+
| `GEMSKILL_TEMPERATURE` | `0.2` | Sampling temperature (skipped for reasoning models that reject it) |
|
|
116
|
+
| `ANTHROPIC_API_KEY` | _(none)_ | API key for Anthropic / Claude models |
|
|
117
|
+
| `OPENAI_API_KEY` | _(none)_ | API key for OpenAI / GPT models |
|
|
118
|
+
| `GEMINI_API_KEY` | _(none)_ | API key for Google Gemini models |
|
|
119
|
+
| `MISTRAL_API_KEY` | _(none)_ | API key for Mistral models |
|
|
120
|
+
| `DEEPSEEK_API_KEY` | _(none)_ | API key for DeepSeek models |
|
|
121
|
+
| `OPENROUTER_API_KEY` | _(none)_ | API key for OpenRouter |
|
|
122
|
+
| `XAI_API_KEY` | _(none)_ | API key for xAI (Grok) models |
|
|
114
123
|
|
|
115
124
|
```bash
|
|
116
125
|
# Store skills on a shared drive accessible to all projects
|
|
@@ -121,6 +130,9 @@ export GEMSKILL_MODEL="claude-sonnet-4-6"
|
|
|
121
130
|
|
|
122
131
|
# Codex users: link skills into a Codex project root instead of .claude/skills
|
|
123
132
|
export GEMSKILL_PROJECT_DIR=".agents" # or ".codex"
|
|
133
|
+
|
|
134
|
+
# Raise the output token cap if generated skill files are being truncated
|
|
135
|
+
export GEMSKIL_MAX_TOKENS=65536
|
|
124
136
|
```
|
|
125
137
|
|
|
126
138
|
The `--model` flag on any command overrides `GEMSKILL_MODEL` for that
|
|
@@ -46,6 +46,7 @@ bundle skill install [OPTIONS]
|
|
|
46
46
|
| `--force` | Regenerate even if skills are already cached |
|
|
47
47
|
| `--verify` | Verify generated skills against each gem's source and fix mismatches (exit `2` if any fixes applied) |
|
|
48
48
|
| `--model MODEL` | LLM model to use (overrides `GEMSKILL_MODEL`) |
|
|
49
|
+
| `--max-tokens TOKENS` | Max output tokens per skill (overrides `GEMSKIL_MAX_TOKENS`; default: 32767) |
|
|
49
50
|
| `--version`, `-v` | Print the installed gem-skill version and exit |
|
|
50
51
|
|
|
51
52
|
**Example:**
|
|
@@ -98,6 +99,7 @@ bundle skill refresh [OPTIONS]
|
|
|
98
99
|
|------|-------------|
|
|
99
100
|
| `--force` | Regenerate all skills, even those already at the correct version |
|
|
100
101
|
| `--model MODEL` | LLM model to use |
|
|
102
|
+
| `--max-tokens TOKENS` | Max output tokens per skill (overrides `GEMSKIL_MAX_TOKENS`; default: 32767) |
|
|
101
103
|
|
|
102
104
|
`refresh` skips gems that are already linked at the correct version (shows
|
|
103
105
|
`up to date`), regenerates gems whose version changed, and removes dead symlinks
|
data/docs/commands/gem-skill.md
CHANGED
|
@@ -20,6 +20,8 @@ gem skill install GEM_NAME [GEM_NAME ...]
|
|
|
20
20
|
| `--force`, `-f` | Regenerate even if a skill is already cached |
|
|
21
21
|
| `--verify` | After generating, verify the skill's code against the gem's actual source and fix mismatches |
|
|
22
22
|
| `--model MODEL`, `-m MODEL` | LLM model to use (overrides `GEMSKILL_MODEL`) |
|
|
23
|
+
| `--max-tokens TOKENS` | Max output tokens for this run (overrides `GEMSKIL_MAX_TOKENS`; default: 32767) |
|
|
24
|
+
| `--temperature TEMP` | Sampling temperature, skipped by reasoning models (overrides `GEMSKILL_TEMPERATURE`; default: 0.2) |
|
|
23
25
|
| `--version`, `-v` | Print the installed gem-skill version and exit |
|
|
24
26
|
|
|
25
27
|
**Examples:**
|
|
@@ -36,6 +38,12 @@ gem skill install rails --force --model claude-opus-4-8
|
|
|
36
38
|
|
|
37
39
|
# Generate, then verify the result against the gem's source code
|
|
38
40
|
gem skill install tty-spinner --verify
|
|
41
|
+
|
|
42
|
+
# Raise the token cap to avoid truncated output
|
|
43
|
+
gem skill install rails --max-tokens 65536
|
|
44
|
+
|
|
45
|
+
# Lower the temperature for more reproducible output (temperature-capable models)
|
|
46
|
+
gem skill install faraday --model claude-sonnet-4-6 --temperature 0.0
|
|
39
47
|
```
|
|
40
48
|
|
|
41
49
|
If a gem is not installed locally, gem-skill will install it automatically
|
data/docs/configuration.md
CHANGED
|
@@ -74,6 +74,30 @@ A blank or unset value falls back to the `.claude/skills` default.
|
|
|
74
74
|
available-skills list or referenced explicitly. See
|
|
75
75
|
[Using with other assistants](skill-files.md#using-with-other-assistants).
|
|
76
76
|
|
|
77
|
+
### `GEMSKIL_MAX_TOKENS`
|
|
78
|
+
|
|
79
|
+
Controls the maximum number of output tokens the LLM may generate for each skill file.
|
|
80
|
+
Increase this if generated `SKILL.md` files are being truncated.
|
|
81
|
+
|
|
82
|
+
| | |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **Default** | `32767` |
|
|
85
|
+
| **Example** | `export GEMSKIL_MAX_TOKENS=65536` |
|
|
86
|
+
|
|
87
|
+
### `GEMSKILL_TEMPERATURE`
|
|
88
|
+
|
|
89
|
+
Sampling temperature for generation. Lower values produce more consistent,
|
|
90
|
+
deterministic output — appropriate for factual reference documentation.
|
|
91
|
+
|
|
92
|
+
| | |
|
|
93
|
+
|---|---|
|
|
94
|
+
| **Default** | `0.2` |
|
|
95
|
+
| **Example** | `export GEMSKILL_TEMPERATURE=0.0` |
|
|
96
|
+
|
|
97
|
+
Only applied to models that support a temperature parameter. Reasoning models
|
|
98
|
+
(e.g. `gpt-5.5`) reject it, so the value is silently skipped for them — switch to
|
|
99
|
+
a temperature-supporting model (Claude, Gemini, older GPT) to benefit.
|
|
100
|
+
|
|
77
101
|
### `GEMSKILL_MODEL`
|
|
78
102
|
|
|
79
103
|
Controls which LLM model is used when generating skills.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# gem/skill first — see gem_command.rb: activation must resolve json < 3
|
|
4
|
+
# before a bare `require "json"` can activate a newer version.
|
|
5
|
+
require "gem/skill"
|
|
3
6
|
require "async"
|
|
4
7
|
require "fileutils"
|
|
5
8
|
require "json"
|
|
6
9
|
require "tty-spinner"
|
|
7
|
-
require "gem/skill"
|
|
8
10
|
|
|
9
11
|
module Gem::Skill
|
|
10
12
|
# Handles `bundle skill SUBCOMMAND` via Bundler's plugin API (plugins.rb).
|
|
@@ -43,11 +45,12 @@ module Gem::Skill
|
|
|
43
45
|
return
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
force
|
|
47
|
-
verify
|
|
48
|
-
model
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
force = opts[:force]
|
|
49
|
+
verify = opts[:verify]
|
|
50
|
+
model = opts[:model] || Generator::DEFAULT_MODEL
|
|
51
|
+
max_tokens = opts[:max_tokens] || Generator::MAX_TOKENS
|
|
52
|
+
errors = []
|
|
53
|
+
results = []
|
|
51
54
|
|
|
52
55
|
multi = TTY::Spinner::Multi.new(
|
|
53
56
|
"[:spinner] Installing skills (#{model})",
|
|
@@ -61,7 +64,7 @@ module Gem::Skill
|
|
|
61
64
|
sp = multi.register(" [:spinner] :title")
|
|
62
65
|
sp.update(title: "#{gem_name} #{version}")
|
|
63
66
|
barrier.async do
|
|
64
|
-
result = install_one(gem_name, version, sp, force: force, model: model, verify: verify)
|
|
67
|
+
result = install_one(gem_name, version, sp, force: force, model: model, verify: verify, max_tokens: max_tokens)
|
|
65
68
|
results << result
|
|
66
69
|
errors << "#{gem_name} #{version}: #{result.error}" if result.error
|
|
67
70
|
end
|
|
@@ -79,11 +82,12 @@ module Gem::Skill
|
|
|
79
82
|
def self.refresh(opts = {})
|
|
80
83
|
gems = Lockfile.gems
|
|
81
84
|
linked = Linker.linked_gems.to_h { |e| [e[:gem_name], e[:version]] }
|
|
82
|
-
force
|
|
83
|
-
verify
|
|
84
|
-
model
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
force = opts[:force]
|
|
86
|
+
verify = opts[:verify]
|
|
87
|
+
model = opts[:model] || Generator::DEFAULT_MODEL
|
|
88
|
+
max_tokens = opts[:max_tokens] || Generator::MAX_TOKENS
|
|
89
|
+
errors = []
|
|
90
|
+
results = []
|
|
87
91
|
|
|
88
92
|
multi = TTY::Spinner::Multi.new(
|
|
89
93
|
"[:spinner] Refreshing skills (#{model})",
|
|
@@ -102,7 +106,7 @@ module Gem::Skill
|
|
|
102
106
|
sp.success("up to date")
|
|
103
107
|
Runner::Result.success
|
|
104
108
|
else
|
|
105
|
-
install_one(gem_name, version, sp, force: force, model: model, verify: verify)
|
|
109
|
+
install_one(gem_name, version, sp, force: force, model: model, verify: verify, max_tokens: max_tokens)
|
|
106
110
|
end
|
|
107
111
|
results << result
|
|
108
112
|
errors << "#{gem_name} #{version}: #{result.error}" if result.error
|
|
@@ -139,9 +143,9 @@ module Gem::Skill
|
|
|
139
143
|
|
|
140
144
|
# --- private ---
|
|
141
145
|
|
|
142
|
-
def self.install_one(gem_name, version, spinner, force:, model:, verify: false)
|
|
146
|
+
def self.install_one(gem_name, version, spinner, force:, model:, verify: false, max_tokens: Generator::MAX_TOKENS)
|
|
143
147
|
spinner.auto_spin
|
|
144
|
-
Runner.install_skill(gem_name, version, spinner, force: force, model: model, verify: verify)
|
|
148
|
+
Runner.install_skill(gem_name, version, spinner, force: force, model: model, verify: verify, max_tokens: max_tokens)
|
|
145
149
|
end
|
|
146
150
|
private_class_method :install_one
|
|
147
151
|
|
|
@@ -178,6 +182,8 @@ module Gem::Skill
|
|
|
178
182
|
when "--version", "-v" then opts[:version] = true
|
|
179
183
|
when /\A--model(?:=(.+))?\z/
|
|
180
184
|
opts[:model] = $1 || args[args.index(arg) + 1]
|
|
185
|
+
when /\A--max-tokens(?:=(.+))?\z/
|
|
186
|
+
opts[:max_tokens] = ($1 || args[args.index(arg) + 1]).to_i
|
|
181
187
|
else
|
|
182
188
|
remaining << arg unless opts[:model].nil? && arg !~ /\A--/
|
|
183
189
|
remaining << arg if arg !~ /\A--/
|
|
@@ -198,10 +204,11 @@ module Gem::Skill
|
|
|
198
204
|
list Show skills linked in this project
|
|
199
205
|
|
|
200
206
|
Options:
|
|
201
|
-
--force
|
|
202
|
-
--verify
|
|
203
|
-
--model MODEL
|
|
204
|
-
--
|
|
207
|
+
--force Regenerate even if already cached
|
|
208
|
+
--verify Verify generated skills against gem source and fix mismatches
|
|
209
|
+
--model MODEL LLM model to use (default: #{Generator::DEFAULT_MODEL})
|
|
210
|
+
--max-tokens TOKENS Max output tokens (overrides GEMSKIL_MAX_TOKENS; default: #{Generator::MAX_TOKENS})
|
|
211
|
+
--version, -v Print gem-skill version and exit
|
|
205
212
|
|
|
206
213
|
Env:
|
|
207
214
|
GEMSKILL_PROJECT_DIR Project dir for symlinks (default: .claude/skills)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rubygems/command"
|
|
4
|
+
# gem/skill must be required first: activating the gem-skill spec resolves
|
|
5
|
+
# json to < 3 (ruby_llm's constraint). A bare `require "json"` before that
|
|
6
|
+
# activates the newest installed json and makes the activation raise
|
|
7
|
+
# Gem::ConflictError, which RubyGems swallows — leaving `gem skill` unregistered.
|
|
8
|
+
require "gem/skill"
|
|
4
9
|
require "async"
|
|
5
10
|
require "fileutils"
|
|
6
11
|
require "json"
|
|
7
12
|
require "tty-spinner"
|
|
8
|
-
require "gem/skill"
|
|
9
13
|
|
|
10
14
|
# Registered as `gem skill` via lib/rubygems_plugin.rb.
|
|
11
15
|
# Manages the global ~/.gem/skills cache.
|
|
@@ -19,6 +23,12 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
19
23
|
add_option("-m", "--model MODEL", "LLM model to use (default: #{Gem::Skill::Generator::DEFAULT_MODEL})") do |model, o|
|
|
20
24
|
o[:model] = model
|
|
21
25
|
end
|
|
26
|
+
add_option("--max-tokens TOKENS", "Max output tokens (overrides GEMSKIL_MAX_TOKENS; default: #{Gem::Skill::Generator::MAX_TOKENS})") do |tokens, o|
|
|
27
|
+
o[:max_tokens] = tokens.to_i
|
|
28
|
+
end
|
|
29
|
+
add_option("--temperature TEMP", "Sampling temperature; ignored by reasoning models (overrides GEMSKILL_TEMPERATURE; default: #{Gem::Skill::Generator::DEFAULT_TEMPERATURE})") do |temp, o|
|
|
30
|
+
o[:temperature] = temp.to_f
|
|
31
|
+
end
|
|
22
32
|
add_option("-v", "--version", "Print gem-skill version and exit") { |_, o| o[:version] = true }
|
|
23
33
|
end
|
|
24
34
|
|
|
@@ -80,9 +90,11 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
80
90
|
return
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
force
|
|
84
|
-
verify
|
|
85
|
-
model
|
|
93
|
+
force = options[:force]
|
|
94
|
+
verify = options[:verify]
|
|
95
|
+
model = options[:model] || Gem::Skill::Generator::DEFAULT_MODEL
|
|
96
|
+
max_tokens = options[:max_tokens] || Gem::Skill::Generator::MAX_TOKENS
|
|
97
|
+
temperature = options[:temperature] || Gem::Skill::Generator::DEFAULT_TEMPERATURE
|
|
86
98
|
|
|
87
99
|
multi = TTY::Spinner::Multi.new(
|
|
88
100
|
"[:spinner] Generating skills (#{model})",
|
|
@@ -96,7 +108,7 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
96
108
|
gem_names.each do |gem_name|
|
|
97
109
|
spinner = multi.register(" [:spinner] :title")
|
|
98
110
|
spinner.update(title: gem_name)
|
|
99
|
-
barrier.async { results << install_one(gem_name, spinner: spinner, force: force, model: model, verify: verify) }
|
|
111
|
+
barrier.async { results << install_one(gem_name, spinner: spinner, force: force, model: model, verify: verify, max_tokens: max_tokens, temperature: temperature) }
|
|
100
112
|
end
|
|
101
113
|
barrier.wait
|
|
102
114
|
ensure
|
|
@@ -112,7 +124,7 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
112
124
|
end
|
|
113
125
|
end
|
|
114
126
|
|
|
115
|
-
def install_one(gem_name, spinner:, force:, model:, verify: false)
|
|
127
|
+
def install_one(gem_name, spinner:, force:, model:, verify: false, max_tokens: Gem::Skill::Generator::MAX_TOKENS, temperature: Gem::Skill::Generator::DEFAULT_TEMPERATURE)
|
|
116
128
|
spinner.auto_spin
|
|
117
129
|
version = resolve_installed_version(gem_name)
|
|
118
130
|
if version.nil?
|
|
@@ -120,7 +132,7 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
120
132
|
version = install_gem(gem_name)
|
|
121
133
|
end
|
|
122
134
|
spinner.update(title: "#{gem_name} #{version}")
|
|
123
|
-
result = Gem::Skill::Runner.install_skill(gem_name, version, spinner, force: force, model: model, verify: verify)
|
|
135
|
+
result = Gem::Skill::Runner.install_skill(gem_name, version, spinner, force: force, model: model, verify: verify, max_tokens: max_tokens, temperature: temperature)
|
|
124
136
|
alert_error "#{gem_name}: #{result.error}" if result.error
|
|
125
137
|
result
|
|
126
138
|
rescue Gem::Skill::Error => e
|
data/lib/gem/skill/generator.rb
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ruby_llm"
|
|
4
|
+
# Provider plugin gems self-register with RubyLLM only when required; without
|
|
5
|
+
# these, "lms/..." and "apfel/..." models are unreachable.
|
|
6
|
+
require "ruby_llm/providers/lms"
|
|
7
|
+
require "ruby_llm/providers/apfel"
|
|
4
8
|
|
|
5
9
|
module Gem::Skill
|
|
6
10
|
# Drives the LLM pipeline: fetches docs, generates a SKILL.md, caches it.
|
|
7
11
|
class Generator
|
|
8
|
-
DEFAULT_MODEL
|
|
9
|
-
|
|
12
|
+
DEFAULT_MODEL = ENV.fetch("GEMSKILL_MODEL", "gpt-5.5")
|
|
13
|
+
MAX_TOKENS = ENV.fetch("GEMSKIL_MAX_TOKENS", 32_767).to_i
|
|
14
|
+
DEFAULT_TEMPERATURE = ENV.fetch("GEMSKILL_TEMPERATURE", 0.2).to_f
|
|
15
|
+
MAX_SOURCE_CHARS = 60_000 # guard against enormous READMEs blowing the context window
|
|
10
16
|
|
|
11
17
|
SYSTEM_INSTRUCTIONS = <<~SYSTEM
|
|
12
18
|
You are a Ruby gem documentation specialist who generates Claude Code skill files.
|
|
@@ -56,12 +62,15 @@ module Gem::Skill
|
|
|
56
62
|
%<sources>s
|
|
57
63
|
PROMPT
|
|
58
64
|
|
|
59
|
-
attr_reader :gem_name, :version, :model
|
|
65
|
+
attr_reader :gem_name, :version, :model, :max_tokens, :temperature
|
|
60
66
|
|
|
61
|
-
def initialize(gem_name, version, model: DEFAULT_MODEL)
|
|
62
|
-
@gem_name
|
|
63
|
-
@version
|
|
64
|
-
@model
|
|
67
|
+
def initialize(gem_name, version, model: DEFAULT_MODEL, max_tokens: MAX_TOKENS, temperature: DEFAULT_TEMPERATURE)
|
|
68
|
+
@gem_name = gem_name
|
|
69
|
+
@version = version
|
|
70
|
+
@model = model
|
|
71
|
+
@model_id, @provider = Gem::Skill.parse_model(model)
|
|
72
|
+
@max_tokens = max_tokens
|
|
73
|
+
@temperature = temperature
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
# Generate and cache a SKILL.md. Returns the skill content string.
|
|
@@ -101,18 +110,41 @@ module Gem::Skill
|
|
|
101
110
|
strip_wrapper_fence(content)
|
|
102
111
|
end
|
|
103
112
|
|
|
104
|
-
# Removes a
|
|
105
|
-
# Belt-and-suspenders: the prompt instructs the model not to wrap,
|
|
106
|
-
#
|
|
113
|
+
# Removes a ```markdown (or ```) fence that wraps the ENTIRE document.
|
|
114
|
+
# Belt-and-suspenders: the prompt instructs the model not to wrap, but some
|
|
115
|
+
# models do it anyway. The trailing ``` is only stripped when a matching
|
|
116
|
+
# opening wrapper fence was present — otherwise a skill that legitimately
|
|
117
|
+
# ends with a code block would lose its closing fence and leave the block
|
|
118
|
+
# open.
|
|
107
119
|
def strip_wrapper_fence(content)
|
|
108
|
-
content
|
|
109
|
-
|
|
120
|
+
stripped = content.strip
|
|
121
|
+
return stripped unless stripped.match?(/\A```(?:markdown)?\s*\n/)
|
|
122
|
+
|
|
123
|
+
stripped
|
|
124
|
+
.sub(/\A```(?:markdown)?\s*\n/, "")
|
|
110
125
|
.sub(/\n```\s*\z/, "")
|
|
111
126
|
.strip
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
def build_chat
|
|
115
|
-
|
|
130
|
+
# with_max_output_tokens (ruby_llm >= 2.0) maps to the right request
|
|
131
|
+
# parameter per provider (max_tokens vs max_completion_tokens).
|
|
132
|
+
chat = RubyLLM.chat(model: @model_id, provider: @provider).with_max_output_tokens(@max_tokens)
|
|
133
|
+
chat = chat.with_temperature(@temperature) if temperature_supported?
|
|
134
|
+
chat.with_instructions(SYSTEM_INSTRUCTIONS)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def model_info
|
|
138
|
+
@model_info ||= RubyLLM.models.find(@model_id, provider: @provider)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Reasoning models (e.g. gpt-5.5) reject a temperature parameter outright.
|
|
142
|
+
# Only set it for models whose metadata doesn't explicitly mark temperature
|
|
143
|
+
# unsupported; treat absent metadata as supported.
|
|
144
|
+
def temperature_supported?
|
|
145
|
+
return false if @temperature.nil?
|
|
146
|
+
|
|
147
|
+
model_info.metadata.fetch(:temperature, true) != false
|
|
116
148
|
end
|
|
117
149
|
|
|
118
150
|
def format_prompt(sources)
|
data/lib/gem/skill/runner.rb
CHANGED
|
@@ -17,14 +17,20 @@ module Gem::Skill
|
|
|
17
17
|
|
|
18
18
|
# Generate + cache + link one skill, optionally verifying it against source.
|
|
19
19
|
# Returns a Runner::Result.
|
|
20
|
-
def self.install_skill(gem_name, version, spinner, force:, model:, verify: false
|
|
20
|
+
def self.install_skill(gem_name, version, spinner, force:, model:, verify: false,
|
|
21
|
+
max_tokens: Generator::MAX_TOKENS, temperature: Generator::DEFAULT_TEMPERATURE)
|
|
21
22
|
if Cache.cached?(gem_name, version) && !force
|
|
22
23
|
Linker.link(gem_name, version)
|
|
23
24
|
content = Cache.read(gem_name, version)
|
|
24
25
|
return finalize(gem_name, version, content, spinner, model: model, verify: verify, status: "already cached")
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
# Stream and discard chunks: a non-streaming request sends nothing over
|
|
29
|
+
# the socket until generation completes, so a slow local model (reasoning
|
|
30
|
+
# models especially) trips Net::ReadTimeout regardless of how high the
|
|
31
|
+
# timeout is set. Streaming keeps bytes flowing between chunks.
|
|
32
|
+
generator = Generator.new(gem_name, version, model: model, max_tokens: max_tokens, temperature: temperature)
|
|
33
|
+
content = generator.generate(force: force) { |_chunk| }
|
|
28
34
|
Linker.link(gem_name, version)
|
|
29
35
|
finalize(gem_name, version, content, spinner, model: model, verify: verify, status: "done")
|
|
30
36
|
rescue => e
|
data/lib/gem/skill/verifier.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ruby_llm"
|
|
4
|
+
require "ruby_llm/providers/lms"
|
|
5
|
+
require "ruby_llm/providers/apfel"
|
|
4
6
|
|
|
5
7
|
module Gem::Skill
|
|
6
8
|
# Second-pass quality gate for a generated SKILL.md.
|
|
@@ -100,7 +102,8 @@ module Gem::Skill
|
|
|
100
102
|
private
|
|
101
103
|
|
|
102
104
|
def build_chat
|
|
103
|
-
|
|
105
|
+
model_id, provider = Gem::Skill.parse_model(model)
|
|
106
|
+
RubyLLM.chat(model: model_id, provider: provider).with_instructions(SYSTEM_INSTRUCTIONS)
|
|
104
107
|
end
|
|
105
108
|
|
|
106
109
|
def format_prompt(skill_content, source)
|
data/lib/gem/skill/version.rb
CHANGED
data/lib/gem/skill.rb
CHANGED
|
@@ -30,14 +30,37 @@ module Gem::Skill
|
|
|
30
30
|
mistral_api_key: "MISTRAL_API_KEY",
|
|
31
31
|
deepseek_api_key: "DEEPSEEK_API_KEY",
|
|
32
32
|
openrouter_api_key: "OPENROUTER_API_KEY",
|
|
33
|
-
xai_api_key: "XAI_API_KEY"
|
|
33
|
+
xai_api_key: "XAI_API_KEY",
|
|
34
|
+
lms_api_base: "LMS_API_BASE",
|
|
35
|
+
lms_api_key: "LMS_API_KEY",
|
|
36
|
+
apfel_api_base: "APFEL_API_BASE",
|
|
37
|
+
apfel_api_key: "APFEL_API_KEY"
|
|
34
38
|
}.freeze
|
|
35
39
|
|
|
40
|
+
# Split a "provider/model" string into [model_id, provider_symbol] when the
|
|
41
|
+
# prefix names a registered RubyLLM provider, e.g.
|
|
42
|
+
# "lms/qwen/qwen3.8-27b" -> ["qwen/qwen3.8-27b", :lms]
|
|
43
|
+
# Otherwise the string is a bare model id: ["gpt-5.5", nil]. A bare id is
|
|
44
|
+
# resolved by RubyLLM's own provider preference, so ids that contain a "/"
|
|
45
|
+
# but don't start with a provider slug (e.g. "qwen/...") pass through intact.
|
|
46
|
+
def self.parse_model(model_string)
|
|
47
|
+
prefix, rest = model_string.to_s.split("/", 2)
|
|
48
|
+
return [model_string, nil] unless rest && RubyLLM::Provider.providers.key?(prefix.to_sym)
|
|
49
|
+
|
|
50
|
+
[rest, prefix.to_sym]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# HTTP read timeout in seconds (GEMSKILL_REQUEST_TIMEOUT to override).
|
|
54
|
+
# RubyLLM's default of 300 is tuned for hosted APIs; a local model (lms/apfel)
|
|
55
|
+
# can legitimately take longer than that to finish one skill generation.
|
|
56
|
+
REQUEST_TIMEOUT = ENV.fetch("GEMSKILL_REQUEST_TIMEOUT", 900).to_i
|
|
57
|
+
|
|
36
58
|
# Configure RubyLLM from environment variables. Called automatically by the
|
|
37
59
|
# CLI commands so users don't need a separate initializer for standalone use.
|
|
38
60
|
# No-op if RubyLLM is already configured (e.g. in a Rails app).
|
|
39
61
|
def self.configure_llm!
|
|
40
62
|
RubyLLM.configure do |config|
|
|
63
|
+
config.request_timeout = REQUEST_TIMEOUT
|
|
41
64
|
ENV_KEY_MAP.each do |attr, env_var|
|
|
42
65
|
value = ENV[env_var]
|
|
43
66
|
config.public_send(:"#{attr}=", value) if value
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem-skill
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -29,14 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '2.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ruby_llm-providers-apfel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ruby_llm-providers-lms
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: tty-spinner
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +93,7 @@ files:
|
|
|
65
93
|
- ".envrc"
|
|
66
94
|
- ".github/workflows/deploy-github-pages.yml"
|
|
67
95
|
- CHANGELOG.md
|
|
96
|
+
- CLAUDE.md
|
|
68
97
|
- LICENSE.txt
|
|
69
98
|
- README.md
|
|
70
99
|
- Rakefile
|
|
@@ -120,14 +149,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
120
149
|
requirements:
|
|
121
150
|
- - ">="
|
|
122
151
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: 3.
|
|
152
|
+
version: '3.4'
|
|
124
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
154
|
requirements:
|
|
126
155
|
- - ">="
|
|
127
156
|
- !ruby/object:Gem::Version
|
|
128
157
|
version: '0'
|
|
129
158
|
requirements: []
|
|
130
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.21
|
|
131
160
|
specification_version: 4
|
|
132
161
|
summary: Generate and manage Claude Code AI skills from Ruby gem documentation.
|
|
133
162
|
test_files: []
|