rubycode 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +38 -8
- data/exe/rubycode_client +274 -0
- data/lib/rubycode/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50f110cea87f9ae98d0613a54a8feec231d1f050c62382b42428d4233bf871c0
|
|
4
|
+
data.tar.gz: d9a0171878bdcbad488afd81146a6887a2ea64a8d361d5932f9e5e162202d36e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eecbe47b72a9ea6b36c905f5b8560a6f8611c381ec3de35d4a50db991dc6576442d04604c51f87ee45f253862c7771cfd70e644833f317d137c33e9d1f40bdf2
|
|
7
|
+
data.tar.gz: 5c15e211f5605d8b986b49704b6d55ac3764364758efaca44d46f552a61ef6eecc993a8bf24eed55452776136859c361f91c1226c94cda7a32edf3f4ecc4c7d9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-03-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **CLI Executable**: `rubycode_client` command for interactive chat after gem installation
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **README**: Reorganized Quick Start section to prioritize CLI usage
|
|
10
|
+
- **Testing**: Updated documentation to use `bin/test` command
|
|
11
|
+
|
|
3
12
|
## [0.1.4] - 2026-03-07
|
|
4
13
|
|
|
5
14
|
### Added
|
data/README.md
CHANGED
|
@@ -59,23 +59,48 @@ Then execute:
|
|
|
59
59
|
bundle install
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Quick Start
|
|
63
63
|
|
|
64
|
-
###
|
|
64
|
+
### Interactive CLI
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
After installing the gem, run the interactive client:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
|
|
69
|
+
rubycode_client
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
The first time you run
|
|
72
|
+
The first time you run it, an interactive setup wizard will guide you through:
|
|
73
73
|
1. Selecting your LLM provider (Ollama Cloud, DeepSeek, Gemini, OpenAI, or OpenRouter)
|
|
74
74
|
2. Choosing a model
|
|
75
75
|
3. Entering API keys (saved securely with encryption)
|
|
76
76
|
|
|
77
77
|
Your configuration is automatically saved and reloaded on subsequent runs.
|
|
78
78
|
|
|
79
|
+
### Programmatic Usage
|
|
80
|
+
|
|
81
|
+
You can also use RubyCode in your Ruby projects:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
require "rubycode"
|
|
85
|
+
|
|
86
|
+
# Configure the LLM adapter
|
|
87
|
+
RubyCode.configure do |config|
|
|
88
|
+
config.adapter = :ollama
|
|
89
|
+
config.url = "https://api.ollama.com"
|
|
90
|
+
config.model = "qwen3-coder:480b-cloud"
|
|
91
|
+
config.root_path = Dir.pwd
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Create a client and ask a question
|
|
95
|
+
client = RubyCode::Client.new
|
|
96
|
+
response = client.ask(prompt: "Find the User model in the codebase")
|
|
97
|
+
puts response
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
### Basic Usage
|
|
103
|
+
|
|
79
104
|
#### Programmatic Usage
|
|
80
105
|
|
|
81
106
|
```ruby
|
|
@@ -163,6 +188,11 @@ export EXA_API_KEY=your_api_key_here
|
|
|
163
188
|
export BRAVE_API_KEY=your_api_key_here
|
|
164
189
|
```
|
|
165
190
|
|
|
191
|
+
### New in 0.1.5
|
|
192
|
+
|
|
193
|
+
- **CLI Executable**: `rubycode_client` command for interactive chat after gem installation
|
|
194
|
+
- **Improved Documentation**: Reorganized Quick Start section with CLI-first approach
|
|
195
|
+
|
|
166
196
|
### New in 0.1.4
|
|
167
197
|
|
|
168
198
|
- **Bug Fix**: Agent now properly stops when `done` tool is called, even if tool execution fails
|
|
@@ -186,10 +216,10 @@ After checking out the repo, run `bundle install` to install dependencies.
|
|
|
186
216
|
|
|
187
217
|
```bash
|
|
188
218
|
# Run all tests
|
|
189
|
-
|
|
219
|
+
bin/test
|
|
190
220
|
|
|
191
|
-
#
|
|
192
|
-
bundle exec rake test
|
|
221
|
+
# Or with rake
|
|
222
|
+
bundle exec rake test
|
|
193
223
|
|
|
194
224
|
# Run specific test file
|
|
195
225
|
bundle exec rake test TEST=test/test_adapters.rb
|
data/exe/rubycode_client
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/rubycode"
|
|
5
|
+
require "tty-prompt"
|
|
6
|
+
|
|
7
|
+
prompt = TTY::Prompt.new
|
|
8
|
+
|
|
9
|
+
# Setup wizard for first-time configuration or reconfiguration
|
|
10
|
+
def setup_wizard(prompt)
|
|
11
|
+
puts RubyCode::Views::Cli::SetupTitle.build
|
|
12
|
+
|
|
13
|
+
adapter = select_adapter(prompt)
|
|
14
|
+
model = select_model_for_adapter(prompt, adapter)
|
|
15
|
+
url = get_url_for_adapter(prompt, adapter)
|
|
16
|
+
|
|
17
|
+
configure_adapter_api_key(prompt, adapter) if adapter_requires_key?(adapter)
|
|
18
|
+
configure_exa_api_key(prompt) if prompt.yes?(I18n.t("rubycode.setup.configure_exa"), default: false)
|
|
19
|
+
|
|
20
|
+
save_and_return_config(adapter, model, url)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def select_adapter(prompt)
|
|
24
|
+
prompt.select(I18n.t("rubycode.setup.adapter_prompt"), {
|
|
25
|
+
I18n.t("rubycode.adapters.ollama.name") => :ollama,
|
|
26
|
+
I18n.t("rubycode.adapters.deepseek.name") => :deepseek,
|
|
27
|
+
I18n.t("rubycode.adapters.gemini.name") => :gemini,
|
|
28
|
+
I18n.t("rubycode.adapters.openai.name") => :openai,
|
|
29
|
+
I18n.t("rubycode.adapters.openrouter.name") => :openrouter
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def select_model_for_adapter(prompt, adapter)
|
|
34
|
+
models = I18n.t("rubycode.models.#{adapter}")
|
|
35
|
+
choices = {}
|
|
36
|
+
models.each do |key, model_data|
|
|
37
|
+
# Skip the :default key which is just a string
|
|
38
|
+
next if %i[default].include?(key) || ["default"].include?(key)
|
|
39
|
+
# Skip if model_data is not a hash (defensive)
|
|
40
|
+
next unless model_data.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
label = model_data[:label] || model_data["label"]
|
|
43
|
+
name = model_data[:name] || model_data["name"]
|
|
44
|
+
choices[label] = name
|
|
45
|
+
end
|
|
46
|
+
prompt.select(I18n.t("rubycode.setup.model_prompt"), choices)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_url_for_adapter(prompt, adapter)
|
|
50
|
+
case adapter
|
|
51
|
+
when :ollama
|
|
52
|
+
prompt.ask(I18n.t("rubycode.setup.url_prompt"), default: "https://api.ollama.com")
|
|
53
|
+
when :deepseek
|
|
54
|
+
"https://api.deepseek.com/v1/chat/completions"
|
|
55
|
+
when :gemini
|
|
56
|
+
"https://generativelanguage.googleapis.com/v1beta/models"
|
|
57
|
+
when :openai
|
|
58
|
+
"https://api.openai.com/v1/chat/completions"
|
|
59
|
+
when :openrouter
|
|
60
|
+
"https://openrouter.ai/api/v1/chat/completions"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def adapter_requires_key?(adapter)
|
|
65
|
+
I18n.t("rubycode.adapters.#{adapter}")[:requires_key]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def configure_adapter_api_key(prompt, adapter)
|
|
69
|
+
env_var_name = "#{adapter.to_s.upcase}_API_KEY"
|
|
70
|
+
saved_exists = RubyCode::Models::ApiKey.key_exists?(adapter: adapter)
|
|
71
|
+
env_exists = ENV.fetch(env_var_name, nil)
|
|
72
|
+
|
|
73
|
+
if saved_exists
|
|
74
|
+
handle_existing_saved_key(prompt, adapter)
|
|
75
|
+
elsif env_exists
|
|
76
|
+
handle_existing_env_key(prompt, adapter, env_var_name)
|
|
77
|
+
else
|
|
78
|
+
prompt_for_required_key(prompt, adapter)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def handle_existing_saved_key(prompt, adapter)
|
|
83
|
+
use_saved = prompt.yes?(I18n.t("rubycode.setup.use_saved_api_key", adapter: adapter.to_s.upcase), default: true)
|
|
84
|
+
return if use_saved
|
|
85
|
+
|
|
86
|
+
new_key = prompt.mask("#{I18n.t("rubycode.setup.api_key_prompt", adapter: adapter.to_s.upcase)} " \
|
|
87
|
+
"#{I18n.t("rubycode.setup.api_key_optional")}")
|
|
88
|
+
RubyCode::Models::ApiKey.save_key(adapter: adapter, api_key: new_key) if new_key && !new_key.empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def handle_existing_env_key(prompt, adapter, env_var_name)
|
|
92
|
+
save_to_db = prompt.yes?(I18n.t("rubycode.setup.save_env_key_to_db", adapter: adapter.to_s.upcase), default: true)
|
|
93
|
+
RubyCode::Models::ApiKey.save_key(adapter: adapter, api_key: ENV.fetch(env_var_name, nil)) if save_to_db
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prompt_for_required_key(prompt, adapter)
|
|
97
|
+
puts RubyCode::Views::Cli::ApiKeyMissing.build(adapter: adapter)
|
|
98
|
+
api_key = prompt.mask(I18n.t("rubycode.setup.api_key_prompt", adapter: adapter.to_s.upcase))
|
|
99
|
+
|
|
100
|
+
if api_key.nil? || api_key.empty?
|
|
101
|
+
puts "\n#{I18n.t("rubycode.setup.api_key_required")}\n"
|
|
102
|
+
exit 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
RubyCode::Models::ApiKey.save_key(adapter: adapter, api_key: api_key)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def configure_exa_api_key(prompt)
|
|
109
|
+
saved_exists = RubyCode::Models::ApiKey.key_exists?(adapter: :exa)
|
|
110
|
+
env_exists = ENV.fetch("EXA_API_KEY", nil)
|
|
111
|
+
|
|
112
|
+
if saved_exists
|
|
113
|
+
handle_existing_saved_key(prompt, :exa)
|
|
114
|
+
elsif env_exists
|
|
115
|
+
handle_exa_env_key(prompt)
|
|
116
|
+
else
|
|
117
|
+
prompt_for_optional_exa_key(prompt)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_exa_env_key(prompt)
|
|
122
|
+
save_to_db = prompt.yes?(I18n.t("rubycode.setup.save_env_key_to_db", adapter: "EXA"), default: true)
|
|
123
|
+
RubyCode::Models::ApiKey.save_key(adapter: :exa, api_key: ENV.fetch("EXA_API_KEY", nil)) if save_to_db
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def prompt_for_optional_exa_key(prompt)
|
|
127
|
+
exa_key = prompt.mask("#{I18n.t("rubycode.setup.api_key_prompt", adapter: "EXA")} " \
|
|
128
|
+
"#{I18n.t("rubycode.setup.api_key_optional")}")
|
|
129
|
+
RubyCode::Models::ApiKey.save_key(adapter: :exa, api_key: exa_key) if exa_key && !exa_key.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def save_and_return_config(adapter, model, url)
|
|
133
|
+
config = { adapter: adapter, model: model, url: url }
|
|
134
|
+
RubyCode::ConfigManager.save(config)
|
|
135
|
+
puts RubyCode::Views::Cli::ConfigSaved.build
|
|
136
|
+
config
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Load or setup configuration
|
|
140
|
+
if RubyCode::ConfigManager.exists?
|
|
141
|
+
saved_config = RubyCode::ConfigManager.load
|
|
142
|
+
use_saved = prompt.yes?(I18n.t("rubycode.setup.use_saved",
|
|
143
|
+
adapter: saved_config[:adapter],
|
|
144
|
+
model: saved_config[:model]),
|
|
145
|
+
default: true)
|
|
146
|
+
config = use_saved ? saved_config : setup_wizard(prompt)
|
|
147
|
+
else
|
|
148
|
+
puts RubyCode::Views::Cli::FirstTimeSetup.build
|
|
149
|
+
config = setup_wizard(prompt)
|
|
150
|
+
end
|
|
151
|
+
adapter = config[:adapter]
|
|
152
|
+
model = config[:model]
|
|
153
|
+
url = config[:url]
|
|
154
|
+
|
|
155
|
+
puts "\n#{RubyCode::Views::Welcome.build}"
|
|
156
|
+
|
|
157
|
+
directory = prompt.ask("What directory do you want to work on?") do |q|
|
|
158
|
+
q.default Dir.pwd
|
|
159
|
+
q.required false
|
|
160
|
+
end
|
|
161
|
+
directory = Dir.pwd if directory.nil? || directory.empty?
|
|
162
|
+
|
|
163
|
+
full_path = File.expand_path(directory)
|
|
164
|
+
|
|
165
|
+
unless Dir.exist?(full_path)
|
|
166
|
+
puts RubyCode::Views::Cli::ErrorMessage.build(message: "Directory '#{full_path}' does not exist!")
|
|
167
|
+
exit 1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
RubyCode.configure do |config|
|
|
171
|
+
config.adapter = adapter
|
|
172
|
+
config.url = url
|
|
173
|
+
config.model = model
|
|
174
|
+
config.root_path = full_path
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
puts RubyCode::Views::Cli::ConfigurationTable.build(
|
|
178
|
+
adapter: adapter,
|
|
179
|
+
model: model,
|
|
180
|
+
directory: full_path
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Ensure database is connected before checking for API keys
|
|
184
|
+
RubyCode::Database.connect
|
|
185
|
+
|
|
186
|
+
# Check if adapter requires API key and ensure it's available
|
|
187
|
+
adapter_info = I18n.t("rubycode.adapters.#{adapter}")
|
|
188
|
+
if adapter_info[:requires_key]
|
|
189
|
+
env_var_name = "#{adapter.to_s.upcase}_API_KEY"
|
|
190
|
+
db_key_exists = RubyCode::Models::ApiKey.key_exists?(adapter: adapter)
|
|
191
|
+
env_key_exists = ENV.fetch(env_var_name, nil)
|
|
192
|
+
|
|
193
|
+
unless db_key_exists || env_key_exists
|
|
194
|
+
# No API key found - prompt user
|
|
195
|
+
puts RubyCode::Views::Cli::ApiKeyMissing.build(adapter: adapter)
|
|
196
|
+
api_key = prompt.mask(I18n.t("rubycode.setup.api_key_prompt", adapter: adapter.to_s.upcase))
|
|
197
|
+
|
|
198
|
+
if api_key.nil? || api_key.empty?
|
|
199
|
+
puts "\n#{I18n.t("rubycode.setup.api_key_required")}\n"
|
|
200
|
+
exit 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Save to database
|
|
204
|
+
RubyCode::Models::ApiKey.save_key(adapter: adapter, api_key: api_key)
|
|
205
|
+
puts "\n✓ API key saved\n"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode)
|
|
210
|
+
|
|
211
|
+
def run_chat_loop(context)
|
|
212
|
+
loop do
|
|
213
|
+
user_input = get_user_input(context.prompt)
|
|
214
|
+
break if user_input.nil?
|
|
215
|
+
|
|
216
|
+
next if handle_special_command(user_input, context)
|
|
217
|
+
|
|
218
|
+
process_user_message(context.client, user_input)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def get_user_input(prompt)
|
|
223
|
+
prompt.ask("You: ")
|
|
224
|
+
rescue StandardError
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def handle_special_command(input, context)
|
|
229
|
+
case input.strip.downcase
|
|
230
|
+
when "exit", "quit"
|
|
231
|
+
puts RubyCode::Views::Cli::ExitMessage.build
|
|
232
|
+
exit 0
|
|
233
|
+
when "clear"
|
|
234
|
+
context.client.clear_memory
|
|
235
|
+
puts RubyCode::Views::Cli::MemoryClearedMessage.build
|
|
236
|
+
true
|
|
237
|
+
when "config"
|
|
238
|
+
show_config_and_reconfigure(context)
|
|
239
|
+
true
|
|
240
|
+
else
|
|
241
|
+
false
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def show_config_and_reconfigure(context)
|
|
246
|
+
puts RubyCode::Views::Cli::ConfigurationTable.build(
|
|
247
|
+
adapter: context.adapter,
|
|
248
|
+
model: context.model,
|
|
249
|
+
directory: context.full_path,
|
|
250
|
+
debug_mode: context.debug_mode
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return unless context.prompt.yes?(I18n.t("rubycode.setup.reconfigure"), default: false)
|
|
254
|
+
|
|
255
|
+
puts RubyCode::Views::Cli::RestartMessage.build
|
|
256
|
+
setup_wizard(context.prompt)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def process_user_message(client, user_input)
|
|
260
|
+
response = client.ask(prompt: user_input)
|
|
261
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
262
|
+
rescue Interrupt
|
|
263
|
+
puts RubyCode::Views::Cli::InterruptMessage.build
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
puts RubyCode::Views::Cli::ErrorDisplay.build(error: e)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
client = RubyCode::Client.new(tty_prompt: prompt)
|
|
269
|
+
|
|
270
|
+
puts RubyCode::Views::Cli::ReadyMessage.build
|
|
271
|
+
|
|
272
|
+
debug_mode = false # Debug mode not yet implemented
|
|
273
|
+
context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode)
|
|
274
|
+
run_chat_loop(context)
|
data/lib/rubycode/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubycode
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Medeiros
|
|
@@ -224,7 +224,8 @@ description: Ruby-native AI coding agent with pluggable LLM adapters, persistent
|
|
|
224
224
|
history, and user approval workflows for file modifications.
|
|
225
225
|
email:
|
|
226
226
|
- jonas.g.medeiros@gmail.com
|
|
227
|
-
executables:
|
|
227
|
+
executables:
|
|
228
|
+
- rubycode_client
|
|
228
229
|
extensions: []
|
|
229
230
|
extra_rdoc_files: []
|
|
230
231
|
files:
|
|
@@ -246,6 +247,7 @@ files:
|
|
|
246
247
|
- config/tools/websearch.json
|
|
247
248
|
- config/tools/write.json
|
|
248
249
|
- docs/images/demo.png
|
|
250
|
+
- exe/rubycode_client
|
|
249
251
|
- lib/rubycode.rb
|
|
250
252
|
- lib/rubycode/adapters/base.rb
|
|
251
253
|
- lib/rubycode/adapters/concerns/debugging.rb
|