rubycode 0.1.3 → 0.1.4
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/.env.example +38 -0
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +52 -0
- data/README.md +101 -23
- data/USAGE.md +1 -23
- data/config/locales/en.yml +208 -1
- data/config/system_prompt.md +6 -1
- data/config/tools/bash.json +1 -1
- data/config/tools/fetch.json +22 -0
- data/config/tools/websearch.json +22 -0
- data/docs/images/demo.png +0 -0
- data/lib/rubycode/adapters/base.rb +92 -2
- data/lib/rubycode/adapters/concerns/debugging.rb +32 -0
- data/lib/rubycode/adapters/concerns/error_handling.rb +89 -0
- data/lib/rubycode/adapters/concerns/http_client.rb +67 -0
- data/lib/rubycode/adapters/deepseek.rb +97 -0
- data/lib/rubycode/adapters/gemini.rb +133 -0
- data/lib/rubycode/adapters/ollama.rb +114 -82
- data/lib/rubycode/adapters/openai.rb +97 -0
- data/lib/rubycode/adapters/openrouter.rb +102 -0
- data/lib/rubycode/agent_loop.rb +110 -18
- data/lib/rubycode/client/approval_handler.rb +14 -0
- data/lib/rubycode/client/display_formatter.rb +18 -10
- data/lib/rubycode/client/response_handler.rb +4 -23
- data/lib/rubycode/client.rb +9 -0
- data/lib/rubycode/config_manager.rb +81 -0
- data/lib/rubycode/configuration.rb +21 -10
- data/lib/rubycode/database.rb +19 -0
- data/lib/rubycode/errors.rb +12 -0
- data/lib/rubycode/models/api_key.rb +118 -0
- data/lib/rubycode/models/memory.rb +84 -10
- data/lib/rubycode/models.rb +1 -0
- data/lib/rubycode/pricing.rb +59 -0
- data/lib/rubycode/search_providers/base.rb +66 -0
- data/lib/rubycode/search_providers/brave_search.rb +60 -0
- data/lib/rubycode/search_providers/concerns/debugging.rb +37 -0
- data/lib/rubycode/search_providers/concerns/error_handling.rb +64 -0
- data/lib/rubycode/search_providers/concerns/http_client.rb +67 -0
- data/lib/rubycode/search_providers/duckduckgo_instant.rb +98 -0
- data/lib/rubycode/search_providers/exa_ai.rb +171 -0
- data/lib/rubycode/search_providers/multi_provider.rb +47 -0
- data/lib/rubycode/token_counter.rb +41 -0
- data/lib/rubycode/tools/bash.rb +38 -8
- data/lib/rubycode/tools/fetch.rb +120 -0
- data/lib/rubycode/tools/web_search.rb +122 -0
- data/lib/rubycode/tools.rb +5 -1
- data/lib/rubycode/value_objects.rb +8 -4
- data/lib/rubycode/version.rb +1 -1
- data/lib/rubycode/views/adapter/debug_delay.rb +20 -0
- data/lib/rubycode/views/adapter/debug_request.rb +33 -0
- data/lib/rubycode/views/adapter/debug_response.rb +31 -0
- data/lib/rubycode/views/agent_loop/token_summary.rb +54 -0
- data/lib/rubycode/views/agent_loop.rb +2 -0
- data/lib/rubycode/views/cli/api_key_missing.rb +37 -0
- data/lib/rubycode/views/cli/config_saved.rb +17 -0
- data/lib/rubycode/views/cli/configuration_table.rb +3 -3
- data/lib/rubycode/views/cli/first_time_setup.rb +17 -0
- data/lib/rubycode/views/cli/restart_message.rb +17 -0
- data/lib/rubycode/views/cli/setup_title.rb +17 -0
- data/lib/rubycode/views/cli.rb +5 -0
- data/lib/rubycode/views/formatter/fetch_summary.rb +54 -0
- data/lib/rubycode/views/formatter/web_search_summary.rb +53 -0
- data/lib/rubycode/views/formatter.rb +2 -0
- data/lib/rubycode/views/search_provider/debug_request.rb +30 -0
- data/lib/rubycode/views/search_provider/debug_response.rb +31 -0
- data/lib/rubycode/views/web_search_approval.rb +29 -0
- data/lib/rubycode/views.rb +5 -0
- data/lib/rubycode.rb +10 -0
- data/rubycode_cli.rb +228 -32
- metadata +81 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Formatter
|
|
8
|
+
# Builds web search result summary display
|
|
9
|
+
class WebSearchSummary
|
|
10
|
+
def self.build(result:)
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
metadata = result.metadata || {}
|
|
13
|
+
|
|
14
|
+
lines = build_header_lines(pastel, metadata)
|
|
15
|
+
lines.concat(build_result_lines(pastel, metadata[:results] || []))
|
|
16
|
+
|
|
17
|
+
lines.join("\n")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.build_header_lines(pastel, metadata)
|
|
21
|
+
count = metadata[:result_count] || 0
|
|
22
|
+
provider = metadata[:provider] || "Search"
|
|
23
|
+
|
|
24
|
+
[
|
|
25
|
+
"",
|
|
26
|
+
" #{pastel.cyan("✓")} Found #{count} result(s) from #{provider}:",
|
|
27
|
+
""
|
|
28
|
+
]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.build_result_lines(pastel, results)
|
|
32
|
+
results.flat_map.with_index do |item, idx|
|
|
33
|
+
build_single_result(pastel, item, idx)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.build_single_result(pastel, item, idx)
|
|
38
|
+
lines = [
|
|
39
|
+
" #{pastel.bold("#{idx + 1}. #{item[:title]}")}",
|
|
40
|
+
pastel.cyan(" #{item[:url]}")
|
|
41
|
+
]
|
|
42
|
+
lines << pastel.dim(" #{item[:snippet]}") if snippet?(item)
|
|
43
|
+
lines << ""
|
|
44
|
+
lines
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.snippet?(item)
|
|
48
|
+
item[:snippet] && !item[:snippet].empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -5,3 +5,5 @@ require_relative "formatter/debug_tool_info"
|
|
|
5
5
|
require_relative "formatter/tool_result"
|
|
6
6
|
require_relative "formatter/info_message"
|
|
7
7
|
require_relative "formatter/minimal_tool_info"
|
|
8
|
+
require_relative "formatter/web_search_summary"
|
|
9
|
+
require_relative "formatter/fetch_summary"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module SearchProvider
|
|
8
|
+
# Builds debug request text for search provider requests
|
|
9
|
+
class DebugRequest
|
|
10
|
+
def self.build(provider_name:, query:, max_results:, url:)
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
content = build_content_lines(pastel, provider_name, query, max_results, url)
|
|
13
|
+
content.join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.build_content_lines(pastel, provider_name, query, max_results, url)
|
|
17
|
+
[
|
|
18
|
+
"",
|
|
19
|
+
pastel.yellow.bold("=== DEBUG SEARCH REQUEST ==="),
|
|
20
|
+
"#{pastel.bold("Provider:")} #{provider_name}",
|
|
21
|
+
"#{pastel.bold("Query:")} #{query}",
|
|
22
|
+
"#{pastel.bold("Max Results:")} #{max_results}",
|
|
23
|
+
"#{pastel.bold("URL:")} #{url}",
|
|
24
|
+
pastel.yellow.bold("=" * 28)
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module SearchProvider
|
|
8
|
+
# Builds debug response text for search provider responses
|
|
9
|
+
class DebugResponse
|
|
10
|
+
def self.build(provider_name:, status_code:, body_preview:)
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
content = build_content_lines(pastel, provider_name, status_code, body_preview)
|
|
13
|
+
content.join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.build_content_lines(pastel, provider_name, status_code, body_preview)
|
|
17
|
+
[
|
|
18
|
+
"",
|
|
19
|
+
pastel.green.bold("=== DEBUG SEARCH RESPONSE ==="),
|
|
20
|
+
"#{pastel.bold("Provider:")} #{provider_name}",
|
|
21
|
+
"#{pastel.bold("Status:")} #{status_code}",
|
|
22
|
+
"",
|
|
23
|
+
pastel.bold("Response Preview:"),
|
|
24
|
+
body_preview,
|
|
25
|
+
pastel.green.bold("=" * 29)
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
# Builds web search approval prompt display
|
|
8
|
+
class WebSearchApproval
|
|
9
|
+
def self.build(query:, max_results:)
|
|
10
|
+
pastel = Pastel.new
|
|
11
|
+
|
|
12
|
+
[
|
|
13
|
+
"",
|
|
14
|
+
pastel.cyan("━" * 80),
|
|
15
|
+
pastel.bold.cyan("🔍 Web Search Request"),
|
|
16
|
+
"#{pastel.cyan("Query:")} #{query}",
|
|
17
|
+
"#{pastel.cyan("Max results:")} #{max_results}",
|
|
18
|
+
pastel.cyan("─" * 80),
|
|
19
|
+
pastel.yellow("This will make HTTP requests to:"),
|
|
20
|
+
pastel.dim(" • DuckDuckGo Instant Answer API (free)"),
|
|
21
|
+
pastel.dim(" • Brave Search API (if BRAVE_API_KEY is set)"),
|
|
22
|
+
pastel.dim(" • Each result URL (HEAD request to verify)"),
|
|
23
|
+
"",
|
|
24
|
+
pastel.cyan("━" * 80)
|
|
25
|
+
].join("\n")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/rubycode/views.rb
CHANGED
|
@@ -6,7 +6,12 @@ require_relative "views/bash_approval"
|
|
|
6
6
|
require_relative "views/write_approval"
|
|
7
7
|
require_relative "views/update_approval"
|
|
8
8
|
require_relative "views/skip_notification"
|
|
9
|
+
require_relative "views/web_search_approval"
|
|
9
10
|
require_relative "views/cli"
|
|
10
11
|
require_relative "views/agent_loop"
|
|
11
12
|
require_relative "views/formatter"
|
|
12
13
|
require_relative "views/response_handler"
|
|
14
|
+
require_relative "views/adapter/debug_request"
|
|
15
|
+
require_relative "views/adapter/debug_response"
|
|
16
|
+
require_relative "views/adapter/debug_delay"
|
|
17
|
+
require_relative "views/agent_loop/token_summary"
|
data/lib/rubycode.rb
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Load environment variables from .env file if present
|
|
4
|
+
require "dotenv/load"
|
|
5
|
+
|
|
3
6
|
require "i18n"
|
|
4
7
|
require_relative "rubycode/version"
|
|
5
8
|
require_relative "rubycode/errors"
|
|
6
9
|
require_relative "rubycode/value_objects"
|
|
7
10
|
require_relative "rubycode/configuration"
|
|
11
|
+
require_relative "rubycode/config_manager"
|
|
8
12
|
require_relative "rubycode/database"
|
|
9
13
|
require_relative "rubycode/models"
|
|
10
14
|
require_relative "rubycode/context_builder"
|
|
15
|
+
require_relative "rubycode/token_counter"
|
|
16
|
+
require_relative "rubycode/pricing"
|
|
11
17
|
require_relative "rubycode/adapters/base"
|
|
12
18
|
require_relative "rubycode/adapters/ollama"
|
|
19
|
+
require_relative "rubycode/adapters/openrouter"
|
|
20
|
+
require_relative "rubycode/adapters/deepseek"
|
|
21
|
+
require_relative "rubycode/adapters/gemini"
|
|
22
|
+
require_relative "rubycode/adapters/openai"
|
|
13
23
|
require_relative "rubycode/tools"
|
|
14
24
|
require_relative "rubycode/agent_loop"
|
|
15
25
|
require_relative "rubycode/client"
|
data/rubycode_cli.rb
CHANGED
|
@@ -5,10 +5,152 @@ require_relative "lib/rubycode"
|
|
|
5
5
|
require "tty-prompt"
|
|
6
6
|
|
|
7
7
|
prompt = TTY::Prompt.new
|
|
8
|
-
adapter = :ollama
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
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]
|
|
12
154
|
|
|
13
155
|
puts "\n#{RubyCode::Views::Welcome.build}"
|
|
14
156
|
|
|
@@ -25,54 +167,108 @@ unless Dir.exist?(full_path)
|
|
|
25
167
|
exit 1
|
|
26
168
|
end
|
|
27
169
|
|
|
28
|
-
debug_mode = prompt.yes?("Enable debug mode?") do |q|
|
|
29
|
-
q.default false
|
|
30
|
-
end
|
|
31
|
-
|
|
32
170
|
RubyCode.configure do |config|
|
|
33
171
|
config.adapter = adapter
|
|
34
172
|
config.url = url
|
|
35
173
|
config.model = model
|
|
36
174
|
config.root_path = full_path
|
|
37
|
-
config.debug = debug_mode
|
|
38
|
-
|
|
39
|
-
config.enable_tool_injection_workaround = true
|
|
40
175
|
end
|
|
41
176
|
|
|
42
177
|
puts RubyCode::Views::Cli::ConfigurationTable.build(
|
|
43
|
-
|
|
178
|
+
adapter: adapter,
|
|
44
179
|
model: model,
|
|
45
|
-
|
|
180
|
+
directory: full_path
|
|
46
181
|
)
|
|
47
182
|
|
|
48
|
-
|
|
183
|
+
# Ensure database is connected before checking for API keys
|
|
184
|
+
RubyCode::Database.connect
|
|
49
185
|
|
|
50
|
-
|
|
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
|
|
51
208
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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)
|
|
57
219
|
end
|
|
58
|
-
|
|
220
|
+
end
|
|
59
221
|
|
|
60
|
-
|
|
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
|
|
61
230
|
when "exit", "quit"
|
|
62
231
|
puts RubyCode::Views::Cli::ExitMessage.build
|
|
63
|
-
|
|
232
|
+
exit 0
|
|
64
233
|
when "clear"
|
|
65
|
-
client.clear_memory
|
|
234
|
+
context.client.clear_memory
|
|
66
235
|
puts RubyCode::Views::Cli::MemoryClearedMessage.build
|
|
67
|
-
|
|
236
|
+
true
|
|
237
|
+
when "config"
|
|
238
|
+
show_config_and_reconfigure(context)
|
|
239
|
+
true
|
|
240
|
+
else
|
|
241
|
+
false
|
|
68
242
|
end
|
|
243
|
+
end
|
|
69
244
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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)
|
|
78
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)
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Medeiros
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.14'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: dotenv
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: nokogiri
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.16'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.16'
|
|
26
68
|
- !ruby/object:Gem::Dependency
|
|
27
69
|
name: sequel
|
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -186,6 +228,7 @@ executables: []
|
|
|
186
228
|
extensions: []
|
|
187
229
|
extra_rdoc_files: []
|
|
188
230
|
files:
|
|
231
|
+
- ".env.example"
|
|
189
232
|
- ".rubocop.yml"
|
|
190
233
|
- CHANGELOG.md
|
|
191
234
|
- LICENSE.txt
|
|
@@ -196,37 +239,63 @@ files:
|
|
|
196
239
|
- config/system_prompt.md
|
|
197
240
|
- config/tools/bash.json
|
|
198
241
|
- config/tools/done.json
|
|
242
|
+
- config/tools/fetch.json
|
|
199
243
|
- config/tools/read.json
|
|
200
244
|
- config/tools/search.json
|
|
201
245
|
- config/tools/update.json
|
|
246
|
+
- config/tools/websearch.json
|
|
202
247
|
- config/tools/write.json
|
|
203
248
|
- docs/images/demo.png
|
|
204
249
|
- lib/rubycode.rb
|
|
205
250
|
- lib/rubycode/adapters/base.rb
|
|
251
|
+
- lib/rubycode/adapters/concerns/debugging.rb
|
|
252
|
+
- lib/rubycode/adapters/concerns/error_handling.rb
|
|
253
|
+
- lib/rubycode/adapters/concerns/http_client.rb
|
|
254
|
+
- lib/rubycode/adapters/deepseek.rb
|
|
255
|
+
- lib/rubycode/adapters/gemini.rb
|
|
206
256
|
- lib/rubycode/adapters/ollama.rb
|
|
257
|
+
- lib/rubycode/adapters/openai.rb
|
|
258
|
+
- lib/rubycode/adapters/openrouter.rb
|
|
207
259
|
- lib/rubycode/agent_loop.rb
|
|
208
260
|
- lib/rubycode/client.rb
|
|
209
261
|
- lib/rubycode/client/approval_handler.rb
|
|
210
262
|
- lib/rubycode/client/display_formatter.rb
|
|
211
263
|
- lib/rubycode/client/response_handler.rb
|
|
264
|
+
- lib/rubycode/config_manager.rb
|
|
212
265
|
- lib/rubycode/configuration.rb
|
|
213
266
|
- lib/rubycode/context_builder.rb
|
|
214
267
|
- lib/rubycode/database.rb
|
|
215
268
|
- lib/rubycode/errors.rb
|
|
216
269
|
- lib/rubycode/models.rb
|
|
270
|
+
- lib/rubycode/models/api_key.rb
|
|
217
271
|
- lib/rubycode/models/base.rb
|
|
218
272
|
- lib/rubycode/models/memory.rb
|
|
273
|
+
- lib/rubycode/pricing.rb
|
|
274
|
+
- lib/rubycode/search_providers/base.rb
|
|
275
|
+
- lib/rubycode/search_providers/brave_search.rb
|
|
276
|
+
- lib/rubycode/search_providers/concerns/debugging.rb
|
|
277
|
+
- lib/rubycode/search_providers/concerns/error_handling.rb
|
|
278
|
+
- lib/rubycode/search_providers/concerns/http_client.rb
|
|
279
|
+
- lib/rubycode/search_providers/duckduckgo_instant.rb
|
|
280
|
+
- lib/rubycode/search_providers/exa_ai.rb
|
|
281
|
+
- lib/rubycode/search_providers/multi_provider.rb
|
|
282
|
+
- lib/rubycode/token_counter.rb
|
|
219
283
|
- lib/rubycode/tools.rb
|
|
220
284
|
- lib/rubycode/tools/base.rb
|
|
221
285
|
- lib/rubycode/tools/bash.rb
|
|
222
286
|
- lib/rubycode/tools/done.rb
|
|
287
|
+
- lib/rubycode/tools/fetch.rb
|
|
223
288
|
- lib/rubycode/tools/read.rb
|
|
224
289
|
- lib/rubycode/tools/search.rb
|
|
225
290
|
- lib/rubycode/tools/update.rb
|
|
291
|
+
- lib/rubycode/tools/web_search.rb
|
|
226
292
|
- lib/rubycode/tools/write.rb
|
|
227
293
|
- lib/rubycode/value_objects.rb
|
|
228
294
|
- lib/rubycode/version.rb
|
|
229
295
|
- lib/rubycode/views.rb
|
|
296
|
+
- lib/rubycode/views/adapter/debug_delay.rb
|
|
297
|
+
- lib/rubycode/views/adapter/debug_request.rb
|
|
298
|
+
- lib/rubycode/views/adapter/debug_response.rb
|
|
230
299
|
- lib/rubycode/views/agent_loop.rb
|
|
231
300
|
- lib/rubycode/views/agent_loop/adapter_error.rb
|
|
232
301
|
- lib/rubycode/views/agent_loop/iteration_footer.rb
|
|
@@ -234,30 +303,41 @@ files:
|
|
|
234
303
|
- lib/rubycode/views/agent_loop/response_received.rb
|
|
235
304
|
- lib/rubycode/views/agent_loop/retry_status.rb
|
|
236
305
|
- lib/rubycode/views/agent_loop/thinking_status.rb
|
|
306
|
+
- lib/rubycode/views/agent_loop/token_summary.rb
|
|
237
307
|
- lib/rubycode/views/agent_loop/tool_error.rb
|
|
238
308
|
- lib/rubycode/views/bash_approval.rb
|
|
239
309
|
- lib/rubycode/views/cli.rb
|
|
310
|
+
- lib/rubycode/views/cli/api_key_missing.rb
|
|
311
|
+
- lib/rubycode/views/cli/config_saved.rb
|
|
240
312
|
- lib/rubycode/views/cli/configuration_table.rb
|
|
241
313
|
- lib/rubycode/views/cli/error_display.rb
|
|
242
314
|
- lib/rubycode/views/cli/error_message.rb
|
|
243
315
|
- lib/rubycode/views/cli/exit_message.rb
|
|
316
|
+
- lib/rubycode/views/cli/first_time_setup.rb
|
|
244
317
|
- lib/rubycode/views/cli/interrupt_message.rb
|
|
245
318
|
- lib/rubycode/views/cli/memory_cleared_message.rb
|
|
246
319
|
- lib/rubycode/views/cli/ready_message.rb
|
|
247
320
|
- lib/rubycode/views/cli/response_box.rb
|
|
321
|
+
- lib/rubycode/views/cli/restart_message.rb
|
|
322
|
+
- lib/rubycode/views/cli/setup_title.rb
|
|
248
323
|
- lib/rubycode/views/formatter.rb
|
|
249
324
|
- lib/rubycode/views/formatter/debug_tool_info.rb
|
|
325
|
+
- lib/rubycode/views/formatter/fetch_summary.rb
|
|
250
326
|
- lib/rubycode/views/formatter/info_message.rb
|
|
251
327
|
- lib/rubycode/views/formatter/minimal_tool_info.rb
|
|
252
328
|
- lib/rubycode/views/formatter/tool_result.rb
|
|
329
|
+
- lib/rubycode/views/formatter/web_search_summary.rb
|
|
253
330
|
- lib/rubycode/views/response_handler.rb
|
|
254
331
|
- lib/rubycode/views/response_handler/agent_finished.rb
|
|
255
332
|
- lib/rubycode/views/response_handler/complete_message.rb
|
|
256
333
|
- lib/rubycode/views/response_handler/max_iterations.rb
|
|
257
334
|
- lib/rubycode/views/response_handler/max_tool_calls.rb
|
|
258
335
|
- lib/rubycode/views/response_handler/tool_injection_warning.rb
|
|
336
|
+
- lib/rubycode/views/search_provider/debug_request.rb
|
|
337
|
+
- lib/rubycode/views/search_provider/debug_response.rb
|
|
259
338
|
- lib/rubycode/views/skip_notification.rb
|
|
260
339
|
- lib/rubycode/views/update_approval.rb
|
|
340
|
+
- lib/rubycode/views/web_search_approval.rb
|
|
261
341
|
- lib/rubycode/views/welcome.rb
|
|
262
342
|
- lib/rubycode/views/write_approval.rb
|
|
263
343
|
- rubycode_cli.rb
|