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.
- checksums.yaml +4 -4
- data/README.md +186 -2
- data/lib/rubyn_code/agent/conversation.rb +2 -1
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +2 -1
- data/lib/rubyn_code/agent/llm_caller.rb +4 -2
- data/lib/rubyn_code/agent/loop.rb +7 -3
- data/lib/rubyn_code/agent/response_modes.rb +2 -1
- data/lib/rubyn_code/agent/system_prompt_builder.rb +39 -0
- data/lib/rubyn_code/agent/tool_processor.rb +4 -2
- data/lib/rubyn_code/cli/app.rb +85 -11
- data/lib/rubyn_code/cli/commands/install_skills.rb +44 -0
- data/lib/rubyn_code/cli/commands/list_skills.rb +149 -0
- data/lib/rubyn_code/cli/commands/provider.rb +2 -1
- data/lib/rubyn_code/cli/commands/remove_skills.rb +35 -0
- data/lib/rubyn_code/cli/commands/skill.rb +4 -2
- data/lib/rubyn_code/cli/commands/skills.rb +104 -0
- data/lib/rubyn_code/cli/repl.rb +11 -1
- data/lib/rubyn_code/cli/repl_commands.rb +2 -1
- data/lib/rubyn_code/cli/repl_setup.rb +38 -1
- data/lib/rubyn_code/config/defaults.rb +2 -0
- data/lib/rubyn_code/config/settings.rb +5 -2
- data/lib/rubyn_code/context/context_budget.rb +2 -1
- data/lib/rubyn_code/context/manager.rb +3 -3
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +6 -3
- data/lib/rubyn_code/ide/handlers/review_handler.rb +19 -2
- data/lib/rubyn_code/ide/protocol.rb +2 -1
- data/lib/rubyn_code/index/codebase_index.rb +2 -1
- data/lib/rubyn_code/learning/extractor.rb +4 -2
- data/lib/rubyn_code/llm/model_router.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +4 -2
- data/lib/rubyn_code/output/diff_renderer.rb +3 -2
- data/lib/rubyn_code/self_test.rb +2 -1
- data/lib/rubyn_code/skills/auto_suggest.rb +131 -0
- data/lib/rubyn_code/skills/catalog.rb +10 -0
- data/lib/rubyn_code/skills/document.rb +8 -2
- data/lib/rubyn_code/skills/gemfile_parser.rb +40 -0
- data/lib/rubyn_code/skills/loader.rb +1 -1
- data/lib/rubyn_code/skills/matcher.rb +89 -0
- data/lib/rubyn_code/skills/pack_context.rb +163 -0
- data/lib/rubyn_code/skills/pack_installer.rb +194 -0
- data/lib/rubyn_code/skills/pack_manager.rb +230 -0
- data/lib/rubyn_code/skills/registry_autoload.rb +112 -0
- data/lib/rubyn_code/skills/registry_client.rb +241 -0
- data/lib/rubyn_code/tools/executor.rb +4 -2
- data/lib/rubyn_code/tools/grep.rb +2 -1
- data/lib/rubyn_code/tools/ide_diagnostics.rb +3 -1
- data/lib/rubyn_code/tools/ide_symbols.rb +3 -1
- data/lib/rubyn_code/tools/load_skill.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +3 -6
- data/lib/rubyn_code/tools/review_pr.rb +15 -4
- data/lib/rubyn_code/tools/web_search.rb +2 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +12 -0
- data/skills/rubyn_self_test.md +75 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 342d51b0944ce35908b8fd56f52b0321cf698ea01ec3cc8fa572d92b9c332b22
|
|
4
|
+
data.tar.gz: '033868c98fedc4814cb9e8c94eb8f013b15641d5a3e7cecf7e819d3d3b57f771'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
@on_tool_result
|
|
139
|
-
@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/rubyn_code/cli/app.rb
CHANGED
|
@@ -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
|
-
|
|
74
|
+
# -- unavoidable dispatch switch
|
|
75
|
+
def dispatch_command(command)
|
|
71
76
|
case command
|
|
72
|
-
when :version
|
|
73
|
-
when :auth
|
|
74
|
-
when :setup
|
|
75
|
-
when :help
|
|
76
|
-
when :run
|
|
77
|
-
when :ide
|
|
78
|
-
when :daemon
|
|
79
|
-
when :
|
|
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
|
-
|
|
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
|