rubyn-code 0.3.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 +263 -21
- data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
- data/lib/rubyn_code/agent/conversation.rb +34 -4
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +57 -3
- data/lib/rubyn_code/agent/llm_caller.rb +11 -1
- data/lib/rubyn_code/agent/loop.rb +14 -3
- data/lib/rubyn_code/agent/response_modes.rb +2 -1
- data/lib/rubyn_code/agent/system_prompt_builder.rb +49 -4
- data/lib/rubyn_code/agent/tool_processor.rb +25 -3
- data/lib/rubyn_code/auth/key_encryption.rb +118 -0
- data/lib/rubyn_code/auth/token_store.rb +50 -9
- data/lib/rubyn_code/autonomous/daemon.rb +117 -14
- data/lib/rubyn_code/autonomous/idle_poller.rb +0 -20
- data/lib/rubyn_code/autonomous/task_claimer.rb +17 -11
- data/lib/rubyn_code/cli/app.rb +116 -11
- data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
- 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/mcp.rb +77 -0
- data/lib/rubyn_code/cli/commands/model.rb +32 -2
- data/lib/rubyn_code/cli/commands/provider.rb +124 -0
- data/lib/rubyn_code/cli/commands/remove_skills.rb +35 -0
- data/lib/rubyn_code/cli/commands/skill.rb +54 -3
- data/lib/rubyn_code/cli/commands/skills.rb +104 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +36 -0
- data/lib/rubyn_code/cli/first_run.rb +159 -0
- data/lib/rubyn_code/cli/repl.rb +15 -0
- data/lib/rubyn_code/cli/repl_commands.rb +3 -1
- data/lib/rubyn_code/cli/repl_lifecycle.rb +1 -0
- data/lib/rubyn_code/cli/repl_setup.rb +74 -1
- data/lib/rubyn_code/config/defaults.rb +3 -0
- data/lib/rubyn_code/config/schema.json +49 -0
- data/lib/rubyn_code/config/settings.rb +12 -6
- data/lib/rubyn_code/config/validator.rb +63 -0
- data/lib/rubyn_code/context/context_budget.rb +18 -2
- data/lib/rubyn_code/context/context_collapse.rb +34 -4
- data/lib/rubyn_code/context/manager.rb +37 -3
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/hooks/registry.rb +4 -0
- data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
- data/lib/rubyn_code/ide/client.rb +110 -0
- data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
- data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
- data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
- data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
- data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
- data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +218 -0
- data/lib/rubyn_code/ide/handlers/review_handler.rb +127 -0
- data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
- data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
- data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
- data/lib/rubyn_code/ide/handlers.rb +76 -0
- data/lib/rubyn_code/ide/protocol.rb +112 -0
- data/lib/rubyn_code/ide/server.rb +186 -0
- data/lib/rubyn_code/index/codebase_index.rb +69 -2
- data/lib/rubyn_code/learning/extractor.rb +4 -2
- data/lib/rubyn_code/llm/adapters/anthropic.rb +6 -2
- data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
- data/lib/rubyn_code/llm/adapters/openai_compatible.rb +6 -2
- data/lib/rubyn_code/llm/client.rb +29 -4
- data/lib/rubyn_code/llm/model_router.rb +2 -1
- data/lib/rubyn_code/mcp/config.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +1 -1
- data/lib/rubyn_code/memory/search.rb +1 -0
- 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 +316 -0
- data/lib/rubyn_code/skills/auto_suggest.rb +131 -0
- data/lib/rubyn_code/skills/catalog.rb +76 -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 +43 -0
- 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/tasks/models.rb +1 -0
- data/lib/rubyn_code/tools/base.rb +13 -0
- data/lib/rubyn_code/tools/bash.rb +5 -0
- data/lib/rubyn_code/tools/edit_file.rb +62 -5
- data/lib/rubyn_code/tools/executor.rb +65 -8
- data/lib/rubyn_code/tools/glob.rb +6 -0
- data/lib/rubyn_code/tools/grep.rb +7 -0
- data/lib/rubyn_code/tools/ide_diagnostics.rb +53 -0
- data/lib/rubyn_code/tools/ide_symbols.rb +55 -0
- data/lib/rubyn_code/tools/load_skill.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +9 -7
- data/lib/rubyn_code/tools/read_file.rb +6 -0
- data/lib/rubyn_code/tools/registry.rb +11 -0
- data/lib/rubyn_code/tools/review_pr.rb +15 -4
- data/lib/rubyn_code/tools/web_search.rb +2 -1
- data/lib/rubyn_code/tools/write_file.rb +17 -0
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +34 -0
- data/skills/rubyn_self_test.md +88 -1
- metadata +43 -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,8 +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 |
|
|
492
|
+
| `/provider` | Add or list providers |
|
|
493
|
+
| `/model` | Show/switch model and provider |
|
|
494
|
+
| `/doctor` | Run environment health checks |
|
|
495
|
+
| `/mcp` | MCP server documentation and status |
|
|
336
496
|
|
|
337
497
|
## Authentication
|
|
338
498
|
|
|
@@ -354,14 +514,50 @@ export OPENAI_API_KEY=sk-...
|
|
|
354
514
|
|
|
355
515
|
Available models: `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-4o`, `gpt-4o-mini`, `o3`, `o4-mini`
|
|
356
516
|
|
|
357
|
-
###
|
|
517
|
+
### Other Providers (Groq, Together, Ollama, etc.)
|
|
358
518
|
|
|
359
|
-
|
|
519
|
+
Add a provider and its API key in one command:
|
|
360
520
|
|
|
361
521
|
```bash
|
|
362
|
-
|
|
522
|
+
/provider add groq https://api.groq.com/openai/v1 --key gsk-xxx --models llama-3.3-70b
|
|
523
|
+
|
|
524
|
+
# For Anthropic-format proxies (e.g., Bedrock, custom gateways)
|
|
525
|
+
/provider add my-proxy https://proxy.example.com/v1 --format anthropic --key sk-xxx --models claude-sonnet-4-6
|
|
526
|
+
|
|
527
|
+
# Update a key later
|
|
528
|
+
/provider set-key groq gsk-new-key
|
|
529
|
+
|
|
530
|
+
# List configured providers
|
|
531
|
+
/provider list
|
|
363
532
|
```
|
|
364
533
|
|
|
534
|
+
API keys are **encrypted at rest** using AES-256-GCM. The encryption key is derived from
|
|
535
|
+
your machine identity (username, hostname, home directory) via PBKDF2, so keys are only
|
|
536
|
+
decryptable on the same machine by the same user. Rubyn decrypts them automatically at
|
|
537
|
+
runtime and re-encrypts on save — no manual steps required.
|
|
538
|
+
|
|
539
|
+
Keys stored via environment variables (`GROQ_API_KEY`, `TOGETHER_API_KEY`, etc.) also work
|
|
540
|
+
as a fallback if you prefer that approach.
|
|
541
|
+
|
|
542
|
+
Or add directly to `~/.rubyn-code/config.yml`:
|
|
543
|
+
|
|
544
|
+
```yaml
|
|
545
|
+
providers:
|
|
546
|
+
groq:
|
|
547
|
+
base_url: https://api.groq.com/openai/v1
|
|
548
|
+
env_key: GROQ_API_KEY
|
|
549
|
+
models:
|
|
550
|
+
top: llama-3.3-70b
|
|
551
|
+
my-proxy:
|
|
552
|
+
api_format: anthropic # 'openai' (default) or 'anthropic'
|
|
553
|
+
base_url: https://proxy.example.com/v1
|
|
554
|
+
env_key: PROXY_API_KEY
|
|
555
|
+
models:
|
|
556
|
+
top: claude-sonnet-4-6
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Then switch with `/model groq:llama-3.3-70b`.
|
|
560
|
+
|
|
365
561
|
Local providers (Ollama, LM Studio) running on `localhost`/`127.0.0.1` don't require an API key.
|
|
366
562
|
|
|
367
563
|
## Architecture
|
|
@@ -391,30 +587,21 @@ Local providers (Ollama, LM Studio) running on `localhost`/`127.0.0.1` don't req
|
|
|
391
587
|
|
|
392
588
|
## Configuration
|
|
393
589
|
|
|
590
|
+
The `provider` and `model` keys at the top set the **default provider and model** used at startup.
|
|
591
|
+
These must match a provider defined in the `providers` section (or a built-in like `anthropic`/`openai`).
|
|
592
|
+
|
|
394
593
|
```yaml
|
|
395
594
|
# ~/.rubyn-code/config.yml (global)
|
|
396
|
-
|
|
595
|
+
provider: anthropic # default provider on startup
|
|
596
|
+
model: claude-opus-4-6 # default model on startup
|
|
397
597
|
permission_mode: allow_read
|
|
398
598
|
session_budget: 5.00
|
|
399
599
|
daily_budget: 10.00
|
|
400
600
|
|
|
401
601
|
# .rubyn-code/config.yml (project — overrides global)
|
|
402
|
-
|
|
602
|
+
provider: minimax # this project uses MiniMax by default
|
|
603
|
+
model: MiniMax-M2.7-highspeed
|
|
403
604
|
permission_mode: autonomous
|
|
404
|
-
|
|
405
|
-
# Use OpenAI instead of Anthropic
|
|
406
|
-
# provider: openai
|
|
407
|
-
# model: gpt-4o
|
|
408
|
-
|
|
409
|
-
# Use an OpenAI-compatible provider
|
|
410
|
-
# provider: groq
|
|
411
|
-
# provider_base_url: https://api.groq.com/openai/v1
|
|
412
|
-
# model: llama-3.3-70b
|
|
413
|
-
|
|
414
|
-
# Local Ollama (no API key needed)
|
|
415
|
-
# provider: ollama
|
|
416
|
-
# provider_base_url: http://localhost:11434/v1
|
|
417
|
-
# model: llama3
|
|
418
605
|
```
|
|
419
606
|
|
|
420
607
|
### Multi-Provider Model Routing
|
|
@@ -469,6 +656,55 @@ providers:
|
|
|
469
656
|
|
|
470
657
|
You can also set custom pricing per model so `/cost` reports accurate spending for third-party providers.
|
|
471
658
|
|
|
659
|
+
## Security
|
|
660
|
+
|
|
661
|
+
### Credential Storage
|
|
662
|
+
|
|
663
|
+
All provider API keys are encrypted at rest using **AES-256-GCM** (authenticated encryption).
|
|
664
|
+
Keys are never stored as plaintext on disk.
|
|
665
|
+
|
|
666
|
+
| Layer | Detail |
|
|
667
|
+
|-------|--------|
|
|
668
|
+
| **Cipher** | AES-256-GCM (authenticated — detects tampering) |
|
|
669
|
+
| **Key derivation** | PBKDF2-HMAC-SHA256, 100,000 iterations |
|
|
670
|
+
| **Machine binding** | Key derived from username + hostname + home directory |
|
|
671
|
+
| **Salt** | Random 32-byte salt, generated once, stored in `~/.rubyn-code/.encryption_salt` |
|
|
672
|
+
| **File permissions** | `tokens.yml` and `.encryption_salt` are `0600` (owner read/write only) |
|
|
673
|
+
|
|
674
|
+
This means:
|
|
675
|
+
- Keys copied to another machine or user account cannot be decrypted
|
|
676
|
+
- The encryption key is never stored — it is derived at runtime
|
|
677
|
+
- Plaintext keys from older versions are automatically encrypted on first read
|
|
678
|
+
|
|
679
|
+
### File Permissions
|
|
680
|
+
|
|
681
|
+
| File | Permissions | Contents |
|
|
682
|
+
|------|------------|----------|
|
|
683
|
+
| `~/.rubyn-code/` | `0700` | Home directory |
|
|
684
|
+
| `~/.rubyn-code/tokens.yml` | `0600` | Encrypted API keys, OAuth tokens |
|
|
685
|
+
| `~/.rubyn-code/.encryption_salt` | `0600` | PBKDF2 salt (not secret alone, but protected) |
|
|
686
|
+
| `~/.rubyn-code/config.yml` | `0600` | Provider config (no secrets) |
|
|
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
|
+
|
|
472
708
|
## Development
|
|
473
709
|
|
|
474
710
|
Requires Ruby 4.0+.
|
|
@@ -480,6 +716,12 @@ bundle install
|
|
|
480
716
|
bundle exec rspec
|
|
481
717
|
```
|
|
482
718
|
|
|
719
|
+
Quick rebuild from source:
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
bin/dev-install
|
|
723
|
+
```
|
|
724
|
+
|
|
483
725
|
## From Rubyn to Rubyn Code
|
|
484
726
|
|
|
485
727
|
If you used the original [Rubyn gem](https://github.com/Rubyn-AI/rubyn), here's what changed:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Expands the tasks CHECK constraint on status to include 'failed',
|
|
4
|
+
# used by the GOLEM daemon to mark tasks that have exceeded max retries.
|
|
5
|
+
#
|
|
6
|
+
# SQLite does not support ALTER CONSTRAINT, so we rebuild the table.
|
|
7
|
+
# The Migrator already wraps .up in a transaction — no manual BEGIN/COMMIT here.
|
|
8
|
+
module Migration013AddFailedStatusToTasks
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def up(db)
|
|
12
|
+
create_new_tasks_table(db)
|
|
13
|
+
migrate_data(db)
|
|
14
|
+
swap_tables(db)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_new_tasks_table(db)
|
|
18
|
+
db.execute(<<~SQL)
|
|
19
|
+
CREATE TABLE tasks_new (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
22
|
+
title TEXT NOT NULL,
|
|
23
|
+
description TEXT,
|
|
24
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
25
|
+
CHECK(status IN ('pending','in_progress','blocked','completed','cancelled','failed')),
|
|
26
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
owner TEXT,
|
|
28
|
+
result TEXT,
|
|
29
|
+
metadata TEXT DEFAULT '{}',
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
|
31
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
32
|
+
)
|
|
33
|
+
SQL
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def migrate_data(db)
|
|
37
|
+
db.execute(<<~SQL)
|
|
38
|
+
INSERT INTO tasks_new (id, session_id, title, description, status, priority, owner, result, metadata, created_at, updated_at)
|
|
39
|
+
SELECT id, session_id, title, description, status, priority, owner, result, metadata, created_at, updated_at
|
|
40
|
+
FROM tasks
|
|
41
|
+
SQL
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def swap_tables(db)
|
|
45
|
+
db.execute('DROP TABLE tasks')
|
|
46
|
+
db.execute('ALTER TABLE tasks_new RENAME TO tasks')
|
|
47
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id)')
|
|
48
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)')
|
|
49
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner)')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -126,19 +126,48 @@ module RubynCode
|
|
|
126
126
|
|
|
127
127
|
# Ensure every tool_use block has a matching tool_result.
|
|
128
128
|
# If a tool_use is orphaned (e.g. from Ctrl-C interruption),
|
|
129
|
-
# inject a synthetic tool_result
|
|
129
|
+
# inject a synthetic tool_result immediately after the assistant
|
|
130
|
+
# message that contains the orphaned tool_use.
|
|
130
131
|
def repair_orphaned_tool_uses(formatted)
|
|
131
132
|
orphaned = collect_tool_use_ids(formatted) - collect_tool_result_ids(formatted)
|
|
132
133
|
return formatted if orphaned.empty?
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
insert_orphan_results(formatted, orphaned)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Walk backwards to find the assistant message containing each orphaned
|
|
139
|
+
# tool_use and insert a user/tool_result message right after it.
|
|
140
|
+
# -- walks messages to find insertion point
|
|
141
|
+
def insert_orphan_results(formatted, orphaned)
|
|
142
|
+
orphan_set = orphaned.to_a.to_set
|
|
143
|
+
insert_idx = find_orphan_insert_index(formatted, orphan_set)
|
|
144
|
+
|
|
145
|
+
results = orphaned.map do |id|
|
|
135
146
|
{ type: 'tool_result', tool_use_id: id, content: '[interrupted]', is_error: true }
|
|
136
147
|
end
|
|
137
148
|
|
|
138
|
-
formatted
|
|
149
|
+
formatted.insert(insert_idx, { role: 'user', content: results })
|
|
139
150
|
formatted
|
|
140
151
|
end
|
|
141
152
|
|
|
153
|
+
# Find the index right after the last assistant message that contains
|
|
154
|
+
# any of the orphaned tool_use IDs.
|
|
155
|
+
def find_orphan_insert_index(formatted, orphan_set)
|
|
156
|
+
formatted.each_with_index.reverse_each do |msg, idx|
|
|
157
|
+
next unless msg[:role] == 'assistant' && msg[:content].is_a?(Array)
|
|
158
|
+
return idx + 1 if assistant_has_orphan?(msg, orphan_set)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
formatted.length # fallback: append at end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def assistant_has_orphan?(msg, orphan_set)
|
|
165
|
+
msg[:content].any? do |block|
|
|
166
|
+
block.is_a?(Hash) && block_matches_type?(block, 'tool_use') &&
|
|
167
|
+
orphan_set.include?(block[:id] || block['id'])
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
142
171
|
def collect_tool_use_ids(formatted)
|
|
143
172
|
collect_block_ids(formatted, role: 'assistant', type: 'tool_use', id_key: :id, id_str_key: 'id')
|
|
144
173
|
end
|
|
@@ -148,7 +177,8 @@ module RubynCode
|
|
|
148
177
|
id_str_key: 'tool_use_id')
|
|
149
178
|
end
|
|
150
179
|
|
|
151
|
-
|
|
180
|
+
# -- iterates blocks with type+role guards
|
|
181
|
+
def collect_block_ids(formatted, role:, type:, id_key:, id_str_key:)
|
|
152
182
|
ids = Set.new
|
|
153
183
|
formatted.each do |msg|
|
|
154
184
|
next unless msg[:role] == role && msg[:content].is_a?(Array)
|
|
@@ -31,8 +31,10 @@ module RubynCode
|
|
|
31
31
|
#
|
|
32
32
|
# @param task_context [Symbol, nil] detected task type
|
|
33
33
|
# @param discovered_tools [Set<String>] tools already discovered this session
|
|
34
|
+
# @param codebase_index [RubynCode::Index::CodebaseIndex, nil] optional index for deeper context detection
|
|
35
|
+
# @param message [String, nil] original user message for index-based matching
|
|
34
36
|
# @return [Array<String>] tool names to include in the schema
|
|
35
|
-
def active_tools(task_context: nil, discovered_tools: Set.new)
|
|
37
|
+
def active_tools(task_context: nil, discovered_tools: Set.new, codebase_index: nil, message: nil)
|
|
36
38
|
tools = BASE_TOOLS.dup
|
|
37
39
|
|
|
38
40
|
# Always include interaction tools
|
|
@@ -45,6 +47,12 @@ module RubynCode
|
|
|
45
47
|
tools.concat(context_tools)
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
# Add index-aware tools when a codebase index and message are available
|
|
51
|
+
if codebase_index && message
|
|
52
|
+
index_contexts = detect_index_contexts(message, codebase_index)
|
|
53
|
+
index_contexts.each { |ctx| tools.concat(resolve_context_tools(ctx)) }
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
# Always include previously discovered tools
|
|
49
57
|
tools.concat(discovered_tools.to_a)
|
|
50
58
|
|
|
@@ -54,8 +62,10 @@ module RubynCode
|
|
|
54
62
|
# Detect task context from a user message.
|
|
55
63
|
#
|
|
56
64
|
# @param message [String]
|
|
65
|
+
# @param codebase_index [RubynCode::Index::CodebaseIndex, nil] optional index for deeper detection
|
|
57
66
|
# @return [Symbol, nil]
|
|
58
|
-
|
|
67
|
+
# -- context detection dispatch
|
|
68
|
+
def detect_context(message, codebase_index: nil)
|
|
59
69
|
msg = message.to_s.downcase
|
|
60
70
|
return :testing if msg.match?(/\b(test|spec|rspec)\b/)
|
|
61
71
|
return :git if msg.match?(/\b(commit|push|diff|branch|merge|git)\b/)
|
|
@@ -65,7 +75,27 @@ module RubynCode
|
|
|
65
75
|
return :explore if msg.match?(/\b(explore|architecture|structure)\b/)
|
|
66
76
|
return :teams if msg.match?(/\b(team|spawn|message|inbox)\b/)
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
# Fall back to index-based detection when keyword matching yields nothing
|
|
79
|
+
return nil unless codebase_index
|
|
80
|
+
|
|
81
|
+
index_contexts = detect_index_contexts(message, codebase_index)
|
|
82
|
+
index_contexts.first
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Detect additional tool contexts based on codebase index content.
|
|
86
|
+
#
|
|
87
|
+
# @param message [String] user message
|
|
88
|
+
# @param codebase_index [RubynCode::Index::CodebaseIndex] codebase index instance
|
|
89
|
+
# @return [Array<Symbol>] detected context symbols
|
|
90
|
+
def detect_index_contexts(message, codebase_index)
|
|
91
|
+
contexts = []
|
|
92
|
+
return contexts unless codebase_index
|
|
93
|
+
|
|
94
|
+
contexts << :rails if message_mentions_model?(message, codebase_index)
|
|
95
|
+
contexts << :testing if message_mentions_specced_file?(message, codebase_index)
|
|
96
|
+
contexts.uniq
|
|
97
|
+
rescue StandardError
|
|
98
|
+
[]
|
|
69
99
|
end
|
|
70
100
|
|
|
71
101
|
# Filter full tool definitions to only include active tools.
|
|
@@ -93,6 +123,30 @@ module RubynCode
|
|
|
93
123
|
[]
|
|
94
124
|
end
|
|
95
125
|
end
|
|
126
|
+
|
|
127
|
+
# Check if the user message mentions a model name from the index.
|
|
128
|
+
def message_mentions_model?(message, codebase_index)
|
|
129
|
+
model_names = codebase_index.nodes
|
|
130
|
+
.select { |n| n['type'] == 'model' }
|
|
131
|
+
.map { |n| n['name'] }
|
|
132
|
+
return false if model_names.empty?
|
|
133
|
+
|
|
134
|
+
msg_lower = message.to_s.downcase
|
|
135
|
+
model_names.any? { |name| msg_lower.include?(name.downcase) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if the user message mentions a file that has specs in the index.
|
|
139
|
+
def message_mentions_specced_file?(message, codebase_index)
|
|
140
|
+
spec_edges = codebase_index.edges.select { |e| e['relationship'] == 'tests' }
|
|
141
|
+
return false if spec_edges.empty?
|
|
142
|
+
|
|
143
|
+
tested_files = spec_edges.map { |e| e['to'] }.compact
|
|
144
|
+
msg_lower = message.to_s.downcase
|
|
145
|
+
tested_files.any? do |file|
|
|
146
|
+
basename = File.basename(file, '.rb')
|
|
147
|
+
msg_lower.include?(basename)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
96
150
|
end
|
|
97
151
|
end
|
|
98
152
|
end
|
|
@@ -43,7 +43,10 @@ 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
|
+
# -- guard clauses for provider/mode checks
|
|
46
47
|
def routed_model
|
|
48
|
+
return nil if manual_model_mode?
|
|
49
|
+
|
|
47
50
|
last_user = last_user_message_text
|
|
48
51
|
return nil unless last_user
|
|
49
52
|
|
|
@@ -60,6 +63,12 @@ module RubynCode
|
|
|
60
63
|
nil
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
def manual_model_mode?
|
|
67
|
+
Config::Settings.new.get('model_mode', 'auto') == 'manual'
|
|
68
|
+
rescue StandardError
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
63
72
|
def last_user_message_text
|
|
64
73
|
msg = @conversation.messages.reverse_each.find { |m| m[:role] == 'user' }
|
|
65
74
|
return nil unless msg
|
|
@@ -68,7 +77,8 @@ module RubynCode
|
|
|
68
77
|
content.is_a?(String) ? content : nil
|
|
69
78
|
end
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
# -- safe accessor checks
|
|
81
|
+
def log_llm_call(opts)
|
|
72
82
|
default_model = @llm_client.respond_to?(:model) ? @llm_client.model : 'default'
|
|
73
83
|
routed = opts[:model]
|
|
74
84
|
effective = routed || default_model
|