rubyn-code 0.4.0 → 0.5.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -2
  3. data/lib/rubyn_code/agent/conversation.rb +2 -1
  4. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +2 -1
  5. data/lib/rubyn_code/agent/llm_caller.rb +4 -2
  6. data/lib/rubyn_code/agent/loop.rb +7 -3
  7. data/lib/rubyn_code/agent/response_modes.rb +2 -1
  8. data/lib/rubyn_code/agent/system_prompt_builder.rb +39 -0
  9. data/lib/rubyn_code/agent/tool_processor.rb +4 -2
  10. data/lib/rubyn_code/cli/app.rb +85 -11
  11. data/lib/rubyn_code/cli/commands/install_skills.rb +44 -0
  12. data/lib/rubyn_code/cli/commands/list_skills.rb +149 -0
  13. data/lib/rubyn_code/cli/commands/provider.rb +2 -1
  14. data/lib/rubyn_code/cli/commands/remove_skills.rb +35 -0
  15. data/lib/rubyn_code/cli/commands/skill.rb +4 -2
  16. data/lib/rubyn_code/cli/commands/skills.rb +104 -0
  17. data/lib/rubyn_code/cli/repl.rb +11 -1
  18. data/lib/rubyn_code/cli/repl_commands.rb +2 -1
  19. data/lib/rubyn_code/cli/repl_setup.rb +38 -1
  20. data/lib/rubyn_code/config/defaults.rb +2 -0
  21. data/lib/rubyn_code/config/settings.rb +5 -2
  22. data/lib/rubyn_code/context/context_budget.rb +2 -1
  23. data/lib/rubyn_code/context/manager.rb +3 -3
  24. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +6 -3
  25. data/lib/rubyn_code/ide/handlers/review_handler.rb +19 -2
  26. data/lib/rubyn_code/ide/protocol.rb +2 -1
  27. data/lib/rubyn_code/index/codebase_index.rb +2 -1
  28. data/lib/rubyn_code/learning/extractor.rb +4 -2
  29. data/lib/rubyn_code/llm/model_router.rb +2 -1
  30. data/lib/rubyn_code/mcp/tool_bridge.rb +1 -1
  31. data/lib/rubyn_code/observability/usage_reporter.rb +4 -2
  32. data/lib/rubyn_code/output/diff_renderer.rb +3 -2
  33. data/lib/rubyn_code/self_test.rb +2 -1
  34. data/lib/rubyn_code/skills/auto_suggest.rb +131 -0
  35. data/lib/rubyn_code/skills/catalog.rb +10 -0
  36. data/lib/rubyn_code/skills/document.rb +8 -2
  37. data/lib/rubyn_code/skills/gemfile_parser.rb +40 -0
  38. data/lib/rubyn_code/skills/loader.rb +1 -1
  39. data/lib/rubyn_code/skills/matcher.rb +89 -0
  40. data/lib/rubyn_code/skills/pack_context.rb +163 -0
  41. data/lib/rubyn_code/skills/pack_installer.rb +194 -0
  42. data/lib/rubyn_code/skills/pack_manager.rb +230 -0
  43. data/lib/rubyn_code/skills/registry_autoload.rb +112 -0
  44. data/lib/rubyn_code/skills/registry_client.rb +241 -0
  45. data/lib/rubyn_code/tools/executor.rb +4 -2
  46. data/lib/rubyn_code/tools/grep.rb +2 -1
  47. data/lib/rubyn_code/tools/ide_diagnostics.rb +3 -1
  48. data/lib/rubyn_code/tools/ide_symbols.rb +3 -1
  49. data/lib/rubyn_code/tools/load_skill.rb +2 -1
  50. data/lib/rubyn_code/tools/output_compressor.rb +3 -6
  51. data/lib/rubyn_code/tools/review_pr.rb +15 -4
  52. data/lib/rubyn_code/tools/web_search.rb +2 -1
  53. data/lib/rubyn_code/version.rb +1 -1
  54. data/lib/rubyn_code.rb +12 -0
  55. data/skills/rubyn_self_test.md +75 -0
  56. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a3ef81b040a1c9a75050f2545f229ba69aa88808ced47ad5cc8634ea52f115c
4
- data.tar.gz: 9020382400cdf70d332858a9b85c89e5aa4a90e07965709e8520a803273d43b2
3
+ metadata.gz: 342d51b0944ce35908b8fd56f52b0321cf698ea01ec3cc8fa572d92b9c332b22
4
+ data.tar.gz: '033868c98fedc4814cb9e8c94eb8f013b15641d5a3e7cecf7e819d3d3b57f771'
5
5
  SHA512:
6
- metadata.gz: d7c2b95f2eec7e20e589c377784a2236f5ca7fa62402012358137a895f18e899e521ab2a3280854dda2376d515d906a40ad56dcc9c21d65062da6c9549a18912
7
- data.tar.gz: cbb80d2749475054829c46aadd8ed5b7244d99f9d6ee05f73a0cd85618866e4e43881ae77e6718dbdcc6cced9e57222752b2a5c403185ae92a039a13da5105e1
6
+ metadata.gz: 283cff44827418497c9ed9d27e4331d8fdf24c04825ee3f28e46a33da28bf61f65d0a2b85173187947393856053fad5466dce5782fcef3d0e1dcce68d37e00eb
7
+ data.tar.gz: 6c96e7beba37f74c0fe947470a1a37d50b521087b573407d6ba7d0d0649d71385b263b70b126fe9c2b7da3c910a233862fdbfb8c1d7c534db6f6cc1d813ec305
data/README.md CHANGED
@@ -22,12 +22,49 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
22
22
 
23
23
  > **Rubyn is going open source.** The original [Rubyn gem](https://github.com/Rubyn-AI/rubyn) provided AI-assisted refactoring, spec generation, and code review through the Rubyn API. **Rubyn Code** is the next evolution — a complete agentic coding assistant that runs locally, thinks for itself, and learns from every session. No API keys. No separate billing. Just `gem install rubyn-code` and go.
24
24
 
25
+ ---
26
+
27
+ ## Table of Contents
28
+
29
+ - [Why Rubyn?](#why-rubyn)
30
+ - [Install](#install)
31
+ - [Quick Start](#quick-start)
32
+ - [What Can Rubyn Do?](#what-can-rubyn-do)
33
+ - [VS Code Extension](#vs-code-extension)
34
+ - [29 Built-in Tools](#29-built-in-tools)
35
+ - [MCP — External Tool Servers](#mcp--external-tool-servers)
36
+ - [Codebase Indexing](#codebase-indexing)
37
+ - [112 Best Practice Skills](#112-best-practice-skills)
38
+ - [Context Architecture](#context-architecture)
39
+ - [RUBYN.md — Project Instructions](#rubynmd--project-instructions)
40
+ - [PR Review](#pr-review)
41
+ - [Sub-Agents & Teams](#sub-agents--teams)
42
+ - [GOLEM — Autonomous Daemon](#golem--autonomous-daemon)
43
+ - [Continuous Learning](#continuous-learning)
44
+ - [Streaming Output](#streaming-output)
45
+ - [Search Providers](#search-providers)
46
+ - [User Hooks](#user-hooks)
47
+ - [CLI Reference](#cli-reference)
48
+ - [Authentication](#authentication)
49
+ - [Architecture](#architecture)
50
+ - [Configuration](#configuration)
51
+ - [Security](#security)
52
+ - [Diagnostics](#diagnostics)
53
+ - [Development](#development)
54
+ - [From Rubyn to Rubyn Code](#from-rubyn-to-rubyn-code)
55
+ - [Contributing](#contributing)
56
+ - [License](#license)
57
+
58
+ ---
59
+
25
60
  ## Why Rubyn?
26
61
 
27
62
  - **Rails-native** — understands service object extraction, RSpec conventions, ActiveRecord patterns, and Hotwire
28
63
  - **Context-aware** — automatically incorporates schema, routes, specs, factories, and models
29
64
  - **Best practices built in** — ships with 112 curated Ruby and Rails guidelines that load on demand
30
65
  - **Agentic** — doesn't just answer questions. Reads files, writes code, runs specs, commits, reviews PRs, spawns sub-agents, and remembers what it learns
66
+ - **IDE-ready** — works in the terminal and inside VS Code with full bidirectional communication
67
+ - **Extensible** — connect external tool servers via MCP, add custom skills, or wire up your own providers
31
68
 
32
69
  ## Install
33
70
 
@@ -93,6 +130,9 @@ rubyn-code --yolo
93
130
 
94
131
  # Single prompt
95
132
  rubyn-code -p "Refactor app/controllers/orders_controller.rb into service objects"
133
+
134
+ # VS Code IDE mode (used by the extension)
135
+ rubyn-code --ide
96
136
  ```
97
137
 
98
138
  ## What Can Rubyn Do?
@@ -118,7 +158,7 @@ rubyn > Write specs for the new service objects
118
158
  > write_file: path=spec/services/orders/create_service_spec.rb
119
159
  > run_specs: path=spec/services/orders/
120
160
 
121
- 4 examples, 0 failures. All green.
161
+ 4 examples, 0 failures. All green.
122
162
  ```
123
163
 
124
164
  ### Review code
@@ -142,6 +182,29 @@ Agent finished (23 tool calls).
142
182
  This is a Rails 7.1 e-commerce app with...
143
183
  ```
144
184
 
185
+ ## VS Code Extension
186
+
187
+ Rubyn Code includes a VS Code extension that provides a full IDE experience with bidirectional JSON-RPC communication. The extension runs Rubyn as a subprocess and connects over stdin/stdout.
188
+
189
+ **Capabilities:**
190
+
191
+ - Chat panel with streaming responses and syntax-highlighted code blocks
192
+ - Inline diffs — review and accept generated code changes directly in the editor
193
+ - Tool approval prompts in the IDE (or skip them in YOLO mode)
194
+ - Full session management — resume, list, fork, and reset conversations
195
+ - Structured code review feedback with severity ratings
196
+ - IDE config get/set for persistent settings
197
+ - All 29 tools available, including MCP tools
198
+
199
+ **Permission modes:**
200
+
201
+ | Mode | Behavior |
202
+ |------|----------|
203
+ | `default` | Per-tool approval required |
204
+ | `bypass` | YOLO — skip all approval prompts |
205
+
206
+ The extension communicates over 14 RPC methods: `initialize`, `prompt`, `cancel`, `review`, `approveToolUse`, `acceptEdit`, `session/*`, `config/*`, `models/list`, and `shutdown`.
207
+
145
208
  ## 29 Built-in Tools
146
209
 
147
210
  | Category | Tools |
@@ -159,6 +222,58 @@ This is a Rails 7.1 e-commerce app with...
159
222
  | **Teams** | `send_message`, `read_inbox` |
160
223
  | **Interactive** | `ask_user` (ask clarifying questions mid-task) |
161
224
 
225
+ ## MCP — External Tool Servers
226
+
227
+ Connect external tool servers via the [Model Context Protocol](https://modelcontextprotocol.io). MCP tools are dynamically discovered and registered as native Rubyn tools, available in the REPL, IDE, and daemon.
228
+
229
+ ### Configuration
230
+
231
+ Create `.rubyn-code/mcp.json` in your project or `~/.rubyn-code/mcp.json` globally:
232
+
233
+ ```json
234
+ {
235
+ "mcpServers": {
236
+ "github": {
237
+ "command": "npx",
238
+ "args": ["-y", "@modelcontextprotocol/server-github"],
239
+ "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
240
+ },
241
+ "my-api": {
242
+ "url": "http://localhost:3001/mcp",
243
+ "timeout": 30
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ - **Stdio transport** — specify `command` and `args` to run a subprocess
250
+ - **SSE transport** — specify `url` for HTTP-based servers
251
+ - Environment variables are interpolated with `${VAR}` syntax
252
+
253
+ MCP tools appear in the tool palette prefixed with `mcp_` and require confirmation before execution. Run `/doctor` to verify server connectivity.
254
+
255
+ Rubyn ships with three example MCP servers: **database explorer**, **RubyGems lookup**, and **Rails routes**. See `/mcp` for documentation.
256
+
257
+ ## Codebase Indexing
258
+
259
+ Rubyn builds a structural index of your codebase on first session and incrementally updates it as files change. The index powers smarter context injection, skill suggestions, and impact analysis.
260
+
261
+ **What it tracks:**
262
+
263
+ - Classes, modules, methods, callbacks, scopes, validations, associations
264
+ - Relationships between files (associations, test coverage, caller/callee)
265
+ - Rails patterns: `has_many`, `belongs_to`, `before_action`, `validates`, etc.
266
+ - File classification: model, controller, service, concern, spec
267
+
268
+ **How it's used:**
269
+
270
+ - Injects a compact structural summary into the system prompt
271
+ - Feeds the dynamic tool schema for smarter tool selection
272
+ - Powers `impact_analysis(file)` to find affected tests and dependents
273
+ - Suggests relevant skills based on the code you're working with
274
+
275
+ Stored at `.rubyn-code/codebase_index.json`. The `/doctor` command flags stale indexes (>24 hours).
276
+
162
277
  ## 112 Best Practice Skills
163
278
 
164
279
  Rubyn ships with curated best practice documents that load on demand. Only skill names are in memory — full content loads when Rubyn needs it.
@@ -176,6 +291,17 @@ Rubyn ships with curated best practice documents that load on demand. Only skill
176
291
  | **Gems** | Sidekiq, Devise, FactoryBot, Pundit, Faraday, Stripe, RuboCop, dry-rb |
177
292
  | **Sinatra** | Application structure, middleware, testing |
178
293
 
294
+ ### Skill search & filter
295
+
296
+ ```
297
+ rubyn > /skill search factory # search by name, description, or tags
298
+ rubyn > /skill list rails # filter by category
299
+ rubyn > /skill list # show all categories
300
+ rubyn > /skill load rspec_matchers # inject a skill into context
301
+ ```
302
+
303
+ Results are relevance-ranked: name matches score highest, then description, then tags.
304
+
179
305
  ### Custom skills
180
306
 
181
307
  Override or extend with your own:
@@ -199,6 +325,8 @@ Rubyn automatically loads relevant context based on what you're working on:
199
325
  - **Service objects** → includes referenced models and their specs
200
326
  - **Any file** → checks for `RUBYN.md`, `CLAUDE.md`, or `AGENT.md` instructions
201
327
 
328
+ The [codebase index](#codebase-indexing) enhances this with structural awareness — Rubyn knows which files depend on each other before it reads them.
329
+
202
330
  ## RUBYN.md — Project Instructions
203
331
 
204
332
  Drop a `RUBYN.md` in your project root and Rubyn follows your conventions:
@@ -259,6 +387,32 @@ rubyn > Send alice a message to write specs for the User model
259
387
 
260
388
  Teammates run in background threads with their own agent loop and mailbox.
261
389
 
390
+ ## GOLEM — Autonomous Daemon
391
+
392
+ GOLEM is an always-on autonomous agent that claims tasks from a queue and works through them independently. It runs a full agent loop per task with access to all tools, MCP servers, and memory.
393
+
394
+ ```bash
395
+ rubyn-code daemon \
396
+ --name golem-1 \
397
+ --role "Backend Engineer" \
398
+ --max-runs 100 \
399
+ --max-cost 10.0 \
400
+ --poll-interval 5 \
401
+ --idle-timeout 60
402
+ ```
403
+
404
+ **Lifecycle:** `spawned → working ⇄ idle → shutting_down → stopped`
405
+
406
+ **Safety limits:**
407
+
408
+ | Guard | Description |
409
+ |-------|-------------|
410
+ | `--max-runs` | Auto-shutdown after N completed tasks |
411
+ | `--max-cost` | Stop when cumulative USD spend exceeds limit |
412
+ | **Retry backoff** | 3 retries per task before marking failed |
413
+ | **Audit trail** | Full conversation saved per task via session persistence |
414
+ | **Cost tracking** | Accurate per-task spend via the observability layer |
415
+
262
416
  ## Continuous Learning
263
417
 
264
418
  Rubyn gets smarter with every session:
@@ -310,12 +464,14 @@ post_tool_use:
310
464
  rubyn-code # Interactive REPL
311
465
  rubyn-code --yolo # Auto-approve all tools
312
466
  rubyn-code -p "prompt" # Single prompt, exit when done
467
+ rubyn-code --ide # IDE server mode (JSON-RPC over stdin/stdout)
313
468
  rubyn-code --resume [ID] # Resume previous session
314
469
  rubyn-code --setup # Pin to this Ruby (run once after install)
315
470
  rubyn-code --debug # Enable debug output
316
471
  rubyn-code --auth # Authenticate with Claude
317
472
  rubyn-code --version # Show version
318
473
  rubyn-code --help # Show help
474
+ rubyn-code daemon [OPTIONS] # Run GOLEM autonomous daemon
319
475
  ```
320
476
 
321
477
  ### Slash Commands
@@ -331,10 +487,12 @@ rubyn-code --help # Show help
331
487
  | `/cost` | Show token usage and costs |
332
488
  | `/tasks` | List all tasks |
333
489
  | `/budget [amt]` | Show or set session budget |
334
- | `/skill [name]` | Load or list available skills |
490
+ | `/skill [name]` | Load, search, or list available skills |
335
491
  | `/resume [id]` | Resume or list sessions |
336
492
  | `/provider` | Add or list providers |
337
493
  | `/model` | Show/switch model and provider |
494
+ | `/doctor` | Run environment health checks |
495
+ | `/mcp` | MCP server documentation and status |
338
496
 
339
497
  ## Authentication
340
498
 
@@ -527,6 +685,26 @@ This means:
527
685
  | `~/.rubyn-code/.encryption_salt` | `0600` | PBKDF2 salt (not secret alone, but protected) |
528
686
  | `~/.rubyn-code/config.yml` | `0600` | Provider config (no secrets) |
529
687
 
688
+ ## Diagnostics
689
+
690
+ Run `/doctor` to check your environment:
691
+
692
+ ```
693
+ rubyn > /doctor
694
+
695
+ ✓ Ruby version 4.0.2
696
+ ✓ Bundler installed
697
+ ✓ Database 12 migrations applied
698
+ ✓ Authentication valid (keychain)
699
+ ✓ Skills 112 available
700
+ ✓ Project detected Rails 7.1
701
+ ✓ MCP servers 2 connected
702
+ ✓ Codebase index fresh (2 hours ago)
703
+ ✓ Skill catalog 112 skills, 0 malformed
704
+ ```
705
+
706
+ Checks Ruby version, bundler, database state, authentication, skills, project type, MCP server connectivity, codebase index freshness, and skill catalog integrity.
707
+
530
708
  ## Development
531
709
 
532
710
  Requires Ruby 4.0+.
@@ -538,6 +716,12 @@ bundle install
538
716
  bundle exec rspec
539
717
  ```
540
718
 
719
+ Quick rebuild from source:
720
+
721
+ ```bash
722
+ bin/dev-install
723
+ ```
724
+
541
725
  ## From Rubyn to Rubyn Code
542
726
 
543
727
  If you used the original [Rubyn gem](https://github.com/Rubyn-AI/rubyn), here's what changed:
@@ -177,7 +177,8 @@ module RubynCode
177
177
  id_str_key: 'tool_use_id')
178
178
  end
179
179
 
180
- def collect_block_ids(formatted, role:, type:, id_key:, id_str_key:) # rubocop:disable Metrics/CyclomaticComplexity -- iterates blocks with type+role guards
180
+ # -- iterates blocks with type+role guards
181
+ def collect_block_ids(formatted, role:, type:, id_key:, id_str_key:)
181
182
  ids = Set.new
182
183
  formatted.each do |msg|
183
184
  next unless msg[:role] == role && msg[:content].is_a?(Array)
@@ -64,7 +64,8 @@ module RubynCode
64
64
  # @param message [String]
65
65
  # @param codebase_index [RubynCode::Index::CodebaseIndex, nil] optional index for deeper detection
66
66
  # @return [Symbol, nil]
67
- def detect_context(message, codebase_index: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- context detection dispatch
67
+ # -- context detection dispatch
68
+ def detect_context(message, codebase_index: nil)
68
69
  msg = message.to_s.downcase
69
70
  return :testing if msg.match?(/\b(test|spec|rspec)\b/)
70
71
  return :git if msg.match?(/\b(commit|push|diff|branch|merge|git)\b/)
@@ -43,7 +43,8 @@ module RubynCode
43
43
  # Only returns models from the active provider — never crosses
44
44
  # provider boundaries (e.g., won't send a GPT model to Anthropic).
45
45
  # Falls back to nil (use client's default) if routing fails.
46
- def routed_model # rubocop:disable Metrics/CyclomaticComplexity -- guard clauses for provider/mode checks
46
+ # -- guard clauses for provider/mode checks
47
+ def routed_model
47
48
  return nil if manual_model_mode?
48
49
 
49
50
  last_user = last_user_message_text
@@ -76,7 +77,8 @@ module RubynCode
76
77
  content.is_a?(String) ? content : nil
77
78
  end
78
79
 
79
- def log_llm_call(opts) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- safe accessor checks
80
+ # -- safe accessor checks
81
+ def log_llm_call(opts)
80
82
  default_model = @llm_client.respond_to?(:model) ? @llm_client.model : 'default'
81
83
  routed = opts[:model]
82
84
  effective = routed || default_model
@@ -59,6 +59,7 @@ module RubynCode
59
59
  inject_skill_listing unless @skills_injected
60
60
  @decision_compactor&.detect_topic_switch(user_input)
61
61
  @skill_ttl&.tick!
62
+ autoload_triggered_skills(user_input)
62
63
  @conversation.add_user_message(user_input)
63
64
  reset_iteration_state
64
65
 
@@ -93,6 +94,8 @@ module RubynCode
93
94
  @background_manager = opts[:background_manager]
94
95
  @stall_detector = opts.fetch(:stall_detector, LoopDetector.new)
95
96
  @skill_loader = opts[:skill_loader]
97
+ @skill_matcher = opts[:skill_matcher]
98
+ @web_skill_autoload = opts[:web_skill_autoload]
96
99
  @project_root = opts[:project_root]
97
100
  @tool_wrapper = opts[:tool_wrapper]
98
101
  @decision_compactor = build_decision_compactor
@@ -134,9 +137,10 @@ module RubynCode
134
137
  end
135
138
 
136
139
  def assign_callbacks(opts)
137
- @on_tool_call = opts[:on_tool_call]
138
- @on_tool_result = opts[:on_tool_result]
139
- @on_text = opts[:on_text]
140
+ @on_tool_call = opts[:on_tool_call]
141
+ @on_tool_result = opts[:on_tool_result]
142
+ @on_text = opts[:on_text]
143
+ @on_skills_autoloaded = opts[:on_skills_autoloaded]
140
144
  @skills_injected = false
141
145
  end
142
146
 
@@ -45,7 +45,8 @@ module RubynCode
45
45
  # @param message [String] the user's input
46
46
  # @param tool_calls [Array] recent tool calls (for context)
47
47
  # @return [Symbol] one of the MODES keys
48
- def detect(message, tool_calls: []) # rubocop:disable Metrics/CyclomaticComplexity -- mode detection dispatch
48
+ # -- mode detection dispatch
49
+ def detect(message, tool_calls: [])
49
50
  return :implementing if implementation_signal?(message)
50
51
  return :debugging if debugging_signal?(message)
51
52
  return :reviewing if reviewing_signal?(message)
@@ -123,6 +123,45 @@ module RubynCode
123
123
  @skills_injected = true
124
124
  end
125
125
 
126
+ # Match the current user message against every skill's :triggers and
127
+ # inject the body of any new match into the conversation so the LLM sees
128
+ # it on the next call. Per-session dedup lives in the Matcher.
129
+ #
130
+ # When the message matches a registry pack the user hasn't installed,
131
+ # @web_skill_autoload silently fetches it, installs it, refreshes the
132
+ # catalog, and surfaces any new skill matches. Web fallback failures
133
+ # are silent so the turn proceeds normally.
134
+ def autoload_triggered_skills(user_input)
135
+ return unless @skill_matcher && @skill_loader
136
+
137
+ matches = @skill_matcher.match(user_input)
138
+ matches += @web_skill_autoload.try(user_input) if @web_skill_autoload
139
+ return if matches.empty?
140
+
141
+ names = matches.map { |m| m[:name] }
142
+ bodies = names.filter_map do |name|
143
+ @skill_loader.load(name)
144
+ rescue StandardError => e
145
+ RubynCode::Debug.warn("Failed to autoload skill '#{name}': #{e.message}")
146
+ nil
147
+ end
148
+ return if bodies.empty?
149
+
150
+ inject_autoloaded_bodies(bodies)
151
+ @on_skills_autoloaded&.call(names)
152
+ end
153
+
154
+ def inject_autoloaded_bodies(bodies)
155
+ @conversation.add_user_message(
156
+ '[system] The following skills are auto-loaded based on the next user ' \
157
+ "message's triggers. Use them as context. Do not mention this message " \
158
+ "to the user.\n\n#{bodies.join("\n\n")}"
159
+ )
160
+ @conversation.add_assistant_message(
161
+ [{ type: 'text', text: 'Understood.' }]
162
+ )
163
+ end
164
+
126
165
  def append_deferred_tools(parts)
127
166
  deferred = deferred_tool_names
128
167
  return if deferred.empty?
@@ -26,7 +26,8 @@ module RubynCode
26
26
  all_tools.select { |t| core_or_discovered?(t) }
27
27
  end
28
28
 
29
- def detect_task_context # rubocop:disable Metrics/CyclomaticComplexity -- safe navigation chain
29
+ # -- safe navigation chain
30
+ def detect_task_context
30
31
  last_msg = @conversation&.messages&.reverse_each&.find { |m| m[:role] == 'user' } # rubocop:disable Style/SafeNavigationChainLength
31
32
  return nil unless last_msg
32
33
 
@@ -142,7 +143,8 @@ module RubynCode
142
143
  end
143
144
  end
144
145
 
145
- def signal_decision_compactor(tool_name, tool_input, result) # rubocop:disable Metrics/CyclomaticComplexity -- tool dispatch
146
+ # -- tool dispatch
147
+ def signal_decision_compactor(tool_name, tool_input, result)
146
148
  return unless @decision_compactor
147
149
 
148
150
  case tool_name
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubynCode
4
4
  module CLI
5
- class App
5
+ class App # rubocop:disable Metrics/ClassLength -- CLI dispatch requires many small methods
6
6
  def self.start(argv)
7
7
  new(argv).run
8
8
  end
@@ -26,6 +26,7 @@ module RubynCode
26
26
  rubyn-code --resume [ID] Resume a previous session
27
27
  rubyn-code --setup Pin rubyn-code to bypass rbenv/rvm
28
28
  rubyn-code --auth Authenticate with Claude
29
+ rubyn-code --install-skills NAME Install a skill pack from the registry
29
30
  rubyn-code --ide Start IDE server (VS Code extension)
30
31
  rubyn-code --permission-mode MODE Set permission mode (default, accept_edits, plan_only, auto, dont_ask, bypass)
31
32
  rubyn-code --version Show version
@@ -47,6 +48,9 @@ module RubynCode
47
48
  /cost Show usage costs
48
49
  /tasks List tasks
49
50
  /skill [name] Load or list skills
51
+ /skills List installed and community skill packs
52
+ /install-skills Install skill packs from rubyn.ai
53
+ /remove-skills Remove an installed skill pack
50
54
 
51
55
  Environment:
52
56
  Config: ~/.rubyn-code/config.yml
@@ -67,16 +71,18 @@ module RubynCode
67
71
 
68
72
  private
69
73
 
70
- def dispatch_command(command) # rubocop:disable Metrics/CyclomaticComplexity -- unavoidable dispatch switch
74
+ # -- unavoidable dispatch switch
75
+ def dispatch_command(command)
71
76
  case command
72
- when :version then puts "rubyn-code #{RubynCode::VERSION}"
73
- when :auth then run_auth
74
- when :setup then run_setup
75
- when :help then display_help
76
- when :run then run_single_prompt(@options[:prompt])
77
- when :ide then run_ide
78
- when :daemon then run_daemon
79
- when :repl then run_repl
77
+ when :version then puts "rubyn-code #{RubynCode::VERSION}"
78
+ when :auth then run_auth
79
+ when :setup then run_setup
80
+ when :help then display_help
81
+ when :run then run_single_prompt(@options[:prompt])
82
+ when :ide then run_ide
83
+ when :daemon then run_daemon
84
+ when :install_skills then run_install_skills(@options[:install_skills_names])
85
+ when :repl then run_repl
80
86
  end
81
87
  end
82
88
 
@@ -116,6 +122,10 @@ module RubynCode
116
122
  options[:command] = :run
117
123
  options[:prompt] = argv[idx + 1]
118
124
  idx + 1
125
+ when '--install-skills'
126
+ options[:command] = :install_skills
127
+ options[:install_skills_names] = collect_pack_names(argv, idx + 1)
128
+ argv.length - 1
119
129
  when 'daemon'
120
130
  options[:command] = :daemon
121
131
  parse_daemon_options!(argv, idx + 1, options)
@@ -155,7 +165,8 @@ module RubynCode
155
165
  idx
156
166
  end
157
167
 
158
- def parse_daemon_value_option(argv, idx, options) # rubocop:disable Metrics/AbcSize -- option dispatch with hash lookup
168
+ # -- option dispatch with hash lookup
169
+ def parse_daemon_value_option(argv, idx, options)
159
170
  arg = argv[idx]
160
171
  daemon = options[:daemon]
161
172
  if DAEMON_INT_FLAGS.key?(arg)
@@ -205,6 +216,69 @@ module RubynCode
205
216
  DaemonRunner.new(@options).run
206
217
  end
207
218
 
219
+ def run_install_skills(names)
220
+ renderer = Renderer.new
221
+ client = Skills::RegistryClient.new
222
+ installer = Skills::PackInstaller.new(
223
+ registry_client: client,
224
+ project_root: Dir.pwd,
225
+ global: @options[:install_skills_global]
226
+ )
227
+
228
+ if names.empty? || names.include?('--update')
229
+ renderer.info('Checking for updates on all installed packs...')
230
+ results = installer.update_all do |event, data|
231
+ report_install_progress(renderer, event, data)
232
+ end
233
+ updated = results.select { |r| r[:status] == :installed }
234
+ if updated.empty?
235
+ renderer.info('All packs are up to date.')
236
+ else
237
+ renderer.info("Updated #{updated.size} pack(s).")
238
+ end
239
+ else
240
+ installer.install(names) do |event, data|
241
+ report_install_progress(renderer, event, data)
242
+ end
243
+ end
244
+ rescue Skills::RegistryError => e
245
+ renderer.error("Registry error: #{e.message}")
246
+ exit(1)
247
+ rescue StandardError => e
248
+ renderer.error("Install failed: #{e.message}")
249
+ exit(1)
250
+ end
251
+
252
+ def report_install_progress(renderer, event, data)
253
+ case event
254
+ when :fetching then renderer.info("Fetching #{data[:name]} from rubyn.ai...")
255
+ when :downloading then renderer.info(" Downloading #{data[:total]} skill files...")
256
+ when :installed
257
+ data[:files].each { |f| puts " → #{data[:name]}/#{f}" }
258
+ renderer.info("Installed #{data[:files].size} skills to .rubyn-code/skills/#{data[:name]}/")
259
+ when :up_to_date then renderer.info("#{data[:name]} is already up to date (v#{data[:version]}).")
260
+ when :error then renderer.error("Failed: #{data[:name]}: #{data[:message]}")
261
+ end
262
+ end
263
+
264
+ def collect_pack_names(argv, start)
265
+ names = []
266
+ idx = start
267
+ while idx < argv.length
268
+ arg = argv[idx]
269
+ break if arg.start_with?('-') && arg != '--update' && arg != '--global'
270
+
271
+ if arg == '--global'
272
+ @options ||= {}
273
+ @options[:install_skills_global] = true
274
+ else
275
+ names << arg
276
+ end
277
+ idx += 1
278
+ end
279
+ names
280
+ end
281
+
208
282
  def run_repl
209
283
  maybe_first_run!
210
284
  REPL.new(
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module CLI
5
+ module Commands
6
+ class InstallSkills < Base
7
+ def self.command_name = '/install-skills'
8
+ def self.description = 'Install skill packs from the Rubyn registry'
9
+
10
+ def execute(args, ctx)
11
+ if args.empty?
12
+ ctx.renderer.warning('Usage: /install-skills <pack-name> [pack-name ...]')
13
+ return
14
+ end
15
+
16
+ pack_manager = RubynCode::Skills::PackManager.new
17
+ registry = RubynCode::Skills::RegistryClient.new
18
+
19
+ args.each { |name| install_pack(name, registry, pack_manager, ctx) }
20
+ rescue RubynCode::Skills::RegistryError => e
21
+ ctx.renderer.error(e.message)
22
+ end
23
+
24
+ private
25
+
26
+ def install_pack(name, registry, pack_manager, ctx)
27
+ if pack_manager.installed?(name)
28
+ ctx.renderer.warning("Pack '#{name}' is already installed. Use /remove-skills first to reinstall.")
29
+ return
30
+ end
31
+
32
+ ctx.renderer.info("Fetching pack '#{name}' from registry...")
33
+ result = registry.fetch_pack(name)
34
+ pack_manager.install(result[:data], etag: result[:etag])
35
+ ctx.renderer.info("Installed skill pack '#{name}' successfully.")
36
+ rescue RubynCode::Skills::RegistryError => e
37
+ ctx.renderer.error("Failed to install '#{name}': #{e.message}")
38
+ rescue ArgumentError => e
39
+ ctx.renderer.error("Invalid pack data for '#{name}': #{e.message}")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end