rubycode 0.1.1 → 0.1.2
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/.rubocop.yml +8 -0
- data/README.md +82 -13
- data/config/tools/bash.json +17 -0
- data/config/tools/done.json +17 -0
- data/config/tools/read.json +25 -0
- data/config/tools/search.json +29 -0
- data/lib/rubycode/adapters/base.rb +2 -1
- data/lib/rubycode/adapters/ollama.rb +45 -38
- data/lib/rubycode/agent_loop.rb +134 -0
- data/lib/rubycode/client/display_formatter.rb +53 -0
- data/lib/rubycode/client/response_handler.rb +55 -0
- data/lib/rubycode/client.rb +24 -172
- data/lib/rubycode/configuration.rb +7 -8
- data/lib/rubycode/context_builder.rb +3 -2
- data/lib/rubycode/errors.rb +21 -0
- data/lib/rubycode/history.rb +16 -14
- data/lib/rubycode/tools/base.rb +80 -0
- data/lib/rubycode/tools/bash.rb +34 -43
- data/lib/rubycode/tools/done.rb +5 -23
- data/lib/rubycode/tools/read.rb +40 -55
- data/lib/rubycode/tools/search.rb +42 -67
- data/lib/rubycode/tools.rb +10 -5
- data/lib/rubycode/value_objects.rb +102 -0
- data/lib/rubycode/version.rb +2 -2
- data/lib/rubycode.rb +5 -3
- data/rubycode_cli.rb +12 -13
- metadata +16 -4
- data/rubycode-0.1.0.gem +0 -0
data/lib/rubycode/client.rb
CHANGED
|
@@ -1,186 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Main client that provides the public API for the agent
|
|
4
5
|
class Client
|
|
5
6
|
attr_reader :history
|
|
6
7
|
|
|
7
8
|
def initialize
|
|
8
|
-
@config =
|
|
9
|
+
@config = RubyCode.config
|
|
9
10
|
@adapter = build_adapter
|
|
10
11
|
@history = History.new
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
MAX_ITERATIONS = 25 # Maximum number of LLM calls per request
|
|
14
|
-
MAX_TOOL_CALLS = 50 # Maximum total tool calls
|
|
15
|
-
|
|
16
14
|
def ask(prompt:)
|
|
17
|
-
# Add user message to history
|
|
18
15
|
@history.add_message(role: "user", content: prompt)
|
|
19
|
-
|
|
20
|
-
# Build system prompt with environment context
|
|
21
16
|
system_prompt = build_system_prompt
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Check iteration limit
|
|
31
|
-
if iteration > MAX_ITERATIONS
|
|
32
|
-
error_msg = "⚠️ Reached maximum iterations (#{MAX_ITERATIONS}). The agent may be stuck in a loop."
|
|
33
|
-
puts "\n#{error_msg}\n"
|
|
34
|
-
@history.add_message(role: "assistant", content: error_msg)
|
|
35
|
-
return error_msg
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Get messages in LLM format
|
|
39
|
-
messages = @history.to_llm_format
|
|
40
|
-
|
|
41
|
-
# Get response from LLM with tools
|
|
42
|
-
response_body = @adapter.generate(
|
|
43
|
-
messages: messages,
|
|
44
|
-
system: system_prompt,
|
|
45
|
-
tools: Tools.definitions
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
# Extract assistant message
|
|
49
|
-
assistant_message = response_body["message"]
|
|
50
|
-
content = assistant_message["content"] || ""
|
|
51
|
-
tool_calls = assistant_message["tool_calls"] || []
|
|
52
|
-
|
|
53
|
-
# Add assistant response to history
|
|
54
|
-
@history.add_message(role: "assistant", content: content)
|
|
55
|
-
|
|
56
|
-
# If no tool calls, we're done
|
|
57
|
-
if tool_calls.empty?
|
|
58
|
-
# ============================================================================
|
|
59
|
-
# WORKAROUND FOR WEAK TOOL-CALLING MODELS (e.g., qwen3-coder)
|
|
60
|
-
# This is ONLY for testing with models that have poor tool-calling capabilities.
|
|
61
|
-
# OpenCode does NOT do this - they rely on good models (Claude, GPT-4).
|
|
62
|
-
# Enable with: config.enable_tool_injection_workaround = true
|
|
63
|
-
# ============================================================================
|
|
64
|
-
if @config.enable_tool_injection_workaround && iteration < 10
|
|
65
|
-
puts " ⚠️ No tool calls - injecting reminder (iteration #{iteration})" unless @config.debug
|
|
66
|
-
|
|
67
|
-
@history.add_message(
|
|
68
|
-
role: "user",
|
|
69
|
-
content: "You MUST call a tool. Do not respond with text. Call search, read, bash, or done tool now."
|
|
70
|
-
)
|
|
71
|
-
next # Continue the loop
|
|
72
|
-
end
|
|
73
|
-
# ============================================================================
|
|
74
|
-
# END WORKAROUND
|
|
75
|
-
# ============================================================================
|
|
76
|
-
|
|
77
|
-
puts "\n✅ Agent finished (#{iteration} iterations, #{total_tool_calls} tool calls)\n" unless @config.debug
|
|
78
|
-
return content
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Check tool call limit
|
|
82
|
-
total_tool_calls += tool_calls.length
|
|
83
|
-
if total_tool_calls > MAX_TOOL_CALLS
|
|
84
|
-
error_msg = "⚠️ Reached maximum tool calls (#{MAX_TOOL_CALLS}). Stopping to prevent excessive operations."
|
|
85
|
-
puts "\n#{error_msg}\n"
|
|
86
|
-
@history.add_message(role: "assistant", content: error_msg)
|
|
87
|
-
return content.empty? ? error_msg : content
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Execute each tool call
|
|
91
|
-
unless @config.debug
|
|
92
|
-
puts "\n🤖 Iteration #{iteration}: Calling #{tool_calls.length} tool(s)..."
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
done_result = nil
|
|
96
|
-
tool_calls.each do |tool_call|
|
|
97
|
-
result = execute_tool(tool_call)
|
|
98
|
-
|
|
99
|
-
# Check if this was the "done" tool
|
|
100
|
-
if tool_call.dig("function", "name") == "done"
|
|
101
|
-
done_result = result
|
|
102
|
-
break
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# If done was called, return the result
|
|
107
|
-
if done_result
|
|
108
|
-
puts "\n✅ Agent finished (#{iteration} iterations, #{total_tool_calls + 1} tool calls)\n" unless @config.debug
|
|
109
|
-
return done_result
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Loop continues - send tool results back to LLM
|
|
113
|
-
end
|
|
18
|
+
AgentLoop.new(
|
|
19
|
+
adapter: @adapter,
|
|
20
|
+
history: @history,
|
|
21
|
+
config: @config,
|
|
22
|
+
system_prompt: system_prompt
|
|
23
|
+
).run
|
|
114
24
|
end
|
|
115
25
|
|
|
116
|
-
private
|
|
117
|
-
|
|
118
26
|
def clear_history
|
|
119
27
|
@history.clear
|
|
120
28
|
end
|
|
121
29
|
|
|
122
30
|
private
|
|
123
31
|
|
|
124
|
-
def execute_tool(tool_call)
|
|
125
|
-
tool_name = tool_call.dig("function", "name")
|
|
126
|
-
arguments = tool_call.dig("function", "arguments")
|
|
127
|
-
|
|
128
|
-
if @config.debug
|
|
129
|
-
puts "\n🔧 Tool: #{tool_name}"
|
|
130
|
-
puts " Args: #{arguments.inspect}"
|
|
131
|
-
else
|
|
132
|
-
# Show minimal output
|
|
133
|
-
case tool_name
|
|
134
|
-
when "bash"
|
|
135
|
-
cmd = arguments.is_a?(Hash) ? arguments["command"] : JSON.parse(arguments)["command"]
|
|
136
|
-
puts " 💻 #{cmd}"
|
|
137
|
-
when "read"
|
|
138
|
-
file = arguments.is_a?(Hash) ? arguments["file_path"] : JSON.parse(arguments)["file_path"]
|
|
139
|
-
puts " 📖 #{file}"
|
|
140
|
-
when "search"
|
|
141
|
-
pattern = arguments.is_a?(Hash) ? arguments["pattern"] : JSON.parse(arguments)["pattern"]
|
|
142
|
-
puts " 🔍 #{pattern}"
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
begin
|
|
147
|
-
# Arguments might be Hash or JSON string
|
|
148
|
-
params = arguments.is_a?(Hash) ? arguments : JSON.parse(arguments)
|
|
149
|
-
|
|
150
|
-
# Execute the tool
|
|
151
|
-
context = { root_path: @config.root_path }
|
|
152
|
-
result = Tools.execute(
|
|
153
|
-
tool_name: tool_name,
|
|
154
|
-
params: params,
|
|
155
|
-
context: context
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
if @config.debug
|
|
159
|
-
puts " ✓ Result: #{result.lines.first&.strip || '(empty)'}#{result.lines.count > 1 ? "... (#{result.lines.count} lines)" : ""}"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Add tool result to history
|
|
163
|
-
@history.add_message(
|
|
164
|
-
role: "user",
|
|
165
|
-
content: "Tool '#{tool_name}' result:\n#{result}"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Return the result so caller can check if it was "done"
|
|
169
|
-
result
|
|
170
|
-
|
|
171
|
-
rescue JSON::ParserError => e
|
|
172
|
-
error_msg = "Error parsing tool arguments: #{e.message}"
|
|
173
|
-
puts " ✗ #{error_msg}"
|
|
174
|
-
@history.add_message(role: "user", content: error_msg)
|
|
175
|
-
nil
|
|
176
|
-
rescue => e
|
|
177
|
-
error_msg = "Error executing tool: #{e.message}"
|
|
178
|
-
puts " ✗ #{error_msg}"
|
|
179
|
-
@history.add_message(role: "user", content: error_msg)
|
|
180
|
-
nil
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
32
|
def build_adapter
|
|
185
33
|
case @config.adapter
|
|
186
34
|
when :ollama
|
|
@@ -193,7 +41,7 @@ module Rubycode
|
|
|
193
41
|
def build_system_prompt
|
|
194
42
|
context = ContextBuilder.new(root_path: @config.root_path).environment_context
|
|
195
43
|
|
|
196
|
-
<<~PROMPT
|
|
44
|
+
<<~PROMPT
|
|
197
45
|
You are a helpful Ruby on Rails coding assistant.
|
|
198
46
|
|
|
199
47
|
#{context}
|
|
@@ -202,17 +50,21 @@ module Rubycode
|
|
|
202
50
|
You MUST call a tool in EVERY response. You MUST NEVER respond with just text.
|
|
203
51
|
|
|
204
52
|
# Available tools
|
|
205
|
-
- bash:
|
|
206
|
-
- search:
|
|
53
|
+
- bash: run any safe command (ls, find, grep, cat, etc.)
|
|
54
|
+
- search: simplified search (use bash + grep for more control)
|
|
207
55
|
- read: view file contents with line numbers
|
|
208
|
-
- done: call
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
1.
|
|
212
|
-
2.
|
|
213
|
-
3.
|
|
214
|
-
4.
|
|
215
|
-
|
|
56
|
+
- done: call when you have the answer
|
|
57
|
+
|
|
58
|
+
# Recommended workflow
|
|
59
|
+
1. Use bash with grep to search: `grep -rn "pattern" directory/`
|
|
60
|
+
2. Use bash with find to locate files: `find . -name "*.rb"`
|
|
61
|
+
3. Once found → use read to see the file
|
|
62
|
+
4. When ready → call done with your final answer
|
|
63
|
+
|
|
64
|
+
# Example searches
|
|
65
|
+
- `grep -rn "button" app/views` - search for "button" in views
|
|
66
|
+
- `grep -ri "new product" .` - case-insensitive search
|
|
67
|
+
- `find . -name "*product*"` - find files with "product" in name
|
|
216
68
|
|
|
217
69
|
IMPORTANT: You cannot respond with plain text. You must ALWAYS call one of the tools.
|
|
218
70
|
When you're ready to provide your answer, call the "done" tool with your answer as the parameter.
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Configuration class for Rubycode settings
|
|
4
5
|
class Configuration
|
|
5
|
-
|
|
6
6
|
attr_accessor :adapter, :url, :model, :root_path, :debug, :enable_tool_injection_workaround
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@adapter = :ollama
|
|
10
10
|
@url = "http://localhost:11434"
|
|
11
|
-
@model = "
|
|
11
|
+
@model = "deepseek-v3.1:671b-cloud"
|
|
12
12
|
@root_path = Dir.pwd
|
|
13
|
-
@debug = false
|
|
13
|
+
@debug = false # Set to true to see JSON requests/responses
|
|
14
14
|
|
|
15
|
-
# WORKAROUND for
|
|
15
|
+
# WORKAROUND for models that don't follow tool-calling instructions
|
|
16
16
|
# When enabled, injects reminder messages if model generates text instead of calling tools
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
@enable_tool_injection_workaround = false
|
|
17
|
+
# Enabled by default as most models need this nudge
|
|
18
|
+
@enable_tool_injection_workaround = true
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Builds environment context for the AI assistant
|
|
4
5
|
class ContextBuilder
|
|
5
6
|
def initialize(root_path:)
|
|
6
7
|
@root_path = root_path
|
|
@@ -12,7 +13,7 @@ module Rubycode
|
|
|
12
13
|
Working directory: #{@root_path}
|
|
13
14
|
Platform: #{RUBY_PLATFORM}
|
|
14
15
|
Ruby version: #{RUBY_VERSION}
|
|
15
|
-
Today's date: #{Time.now.strftime(
|
|
16
|
+
Today's date: #{Time.now.strftime("%Y-%m-%d")}
|
|
16
17
|
</env>
|
|
17
18
|
CONTEXT
|
|
18
19
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Base error class for all RubyCode errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Base class for all tool-related errors
|
|
8
|
+
class ToolError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when attempting to execute an unsafe bash command
|
|
11
|
+
class UnsafeCommandError < ToolError; end
|
|
12
|
+
|
|
13
|
+
# Raised when a file is not found
|
|
14
|
+
class FileNotFoundError < ToolError; end
|
|
15
|
+
|
|
16
|
+
# Raised when a path is invalid or outside allowed directory
|
|
17
|
+
class PathError < ToolError; end
|
|
18
|
+
|
|
19
|
+
# Raised when command execution fails
|
|
20
|
+
class CommandExecutionError < ToolError; end
|
|
21
|
+
end
|
data/lib/rubycode/history.rb
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Manages conversation history between user and assistant
|
|
4
5
|
class History
|
|
6
|
+
attr_reader :messages
|
|
7
|
+
|
|
5
8
|
def initialize
|
|
6
9
|
@messages = []
|
|
7
10
|
end
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Accept either Message objects or keyword arguments for backwards compatibility
|
|
13
|
+
def add_message(message = nil, role: nil, content: nil)
|
|
14
|
+
if message.is_a?(Message)
|
|
15
|
+
@messages << message
|
|
16
|
+
elsif role && content
|
|
17
|
+
@messages << Message.new(role: role, content: content)
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError, "Must provide either a Message object or role: and content: keyword arguments"
|
|
20
|
+
end
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
def to_llm_format
|
|
18
|
-
@messages.map
|
|
24
|
+
@messages.map(&:to_h)
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def clear
|
|
22
28
|
@messages = []
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
def messages
|
|
26
|
-
@messages
|
|
27
|
-
end
|
|
28
|
-
|
|
29
31
|
def last_user_message
|
|
30
|
-
@messages.reverse.find { |msg| msg
|
|
32
|
+
@messages.reverse.find { |msg| msg.role == "user" }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def last_assistant_message
|
|
34
|
-
@messages.reverse.find { |msg| msg
|
|
36
|
+
@messages.reverse.find { |msg| msg.role == "assistant" }
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Tools
|
|
7
|
+
# Base class for all tools providing common functionality
|
|
8
|
+
class Base
|
|
9
|
+
attr_reader :context
|
|
10
|
+
|
|
11
|
+
def initialize(context:)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Public execute method that validates and calls perform
|
|
16
|
+
def execute(params)
|
|
17
|
+
validate_params!(params)
|
|
18
|
+
result = perform(params)
|
|
19
|
+
wrap_result(result)
|
|
20
|
+
rescue ToolError => e
|
|
21
|
+
# Re-raise tool errors as-is
|
|
22
|
+
raise e
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
# Wrap unexpected errors in ToolError
|
|
25
|
+
raise ToolError, "Error in #{self.class.name}: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Load tool schema from JSON file in config/tools/
|
|
29
|
+
def self.definition
|
|
30
|
+
@definition ||= load_schema
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Load schema from config/tools/{tool_name}.json
|
|
34
|
+
def self.load_schema
|
|
35
|
+
tool_name = name.split("::").last.downcase
|
|
36
|
+
schema_path = File.join(__dir__, "..", "..", "..", "config", "tools", "#{tool_name}.json")
|
|
37
|
+
|
|
38
|
+
raise ToolError, "Schema file not found: #{schema_path}" unless File.exist?(schema_path)
|
|
39
|
+
|
|
40
|
+
JSON.parse(File.read(schema_path), symbolize_names: true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Must be implemented by subclasses to perform the actual work
|
|
46
|
+
def perform(params)
|
|
47
|
+
raise NotImplementedError, "#{self.class.name} must implement #perform"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Validates required parameters based on schema
|
|
51
|
+
def validate_params!(params)
|
|
52
|
+
return unless self.class.definition.dig(:function, :parameters, :required)
|
|
53
|
+
|
|
54
|
+
required_params = self.class.definition.dig(:function, :parameters, :required)
|
|
55
|
+
required_params.each do |param|
|
|
56
|
+
next if params.key?(param) || params.key?(param.to_s)
|
|
57
|
+
|
|
58
|
+
raise ToolError, "Missing required parameter: #{param}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Wraps string results in ToolResult, passes through ToolResult and CommandResult
|
|
63
|
+
def wrap_result(result)
|
|
64
|
+
case result
|
|
65
|
+
when ToolResult, CommandResult
|
|
66
|
+
result
|
|
67
|
+
when String
|
|
68
|
+
ToolResult.new(content: result)
|
|
69
|
+
else
|
|
70
|
+
ToolResult.new(content: result.to_s)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Helper to get root path from context
|
|
75
|
+
def root_path
|
|
76
|
+
context[:root_path]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/rubycode/tools/bash.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require "shellwords"
|
|
4
5
|
|
|
5
|
-
module
|
|
6
|
+
module RubyCode
|
|
6
7
|
module Tools
|
|
7
|
-
|
|
8
|
+
# Tool for executing safe bash commands
|
|
9
|
+
class Bash < Base
|
|
8
10
|
# Whitelist of safe commands
|
|
9
11
|
SAFE_COMMANDS = %w[
|
|
10
12
|
ls
|
|
@@ -18,57 +20,46 @@ module Rubycode
|
|
|
18
20
|
file
|
|
19
21
|
which
|
|
20
22
|
echo
|
|
23
|
+
grep
|
|
24
|
+
rg
|
|
21
25
|
].freeze
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
type: "function",
|
|
26
|
-
function: {
|
|
27
|
-
name: "bash",
|
|
28
|
-
description: "Execute safe bash commands for exploring the filesystem and terminal operations.\n\nIMPORTANT: This tool is for terminal operations and directory exploration (ls, find, tree, etc.). DO NOT use it for file operations (reading, searching file contents) - use the specialized tools instead.\n\nWhitelisted commands: #{SAFE_COMMANDS.join(', ')}",
|
|
29
|
-
parameters: {
|
|
30
|
-
type: "object",
|
|
31
|
-
properties: {
|
|
32
|
-
command: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "The bash command to execute (e.g., 'ls -la', 'find . -name \"*.rb\"', 'tree app')"
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
required: ["command"]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
end
|
|
27
|
+
private
|
|
42
28
|
|
|
43
|
-
def
|
|
29
|
+
def perform(params)
|
|
44
30
|
command = params["command"].strip
|
|
45
|
-
|
|
46
|
-
# Extract the base command (first word)
|
|
47
31
|
base_command = command.split.first
|
|
48
32
|
|
|
49
|
-
unless SAFE_COMMANDS.include?(base_command)
|
|
50
|
-
return "Error: Command '#{base_command}' is not allowed. Safe commands: #{SAFE_COMMANDS.join(', ')}"
|
|
51
|
-
end
|
|
33
|
+
raise UnsafeCommandError, safe_command_error(base_command) unless SAFE_COMMANDS.include?(base_command)
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
35
|
+
execute_command(command)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def safe_command_error(base_command)
|
|
39
|
+
"Command '#{base_command}' is not allowed. Safe commands: #{SAFE_COMMANDS.join(", ")}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def execute_command(command)
|
|
43
|
+
Dir.chdir(root_path) do
|
|
55
44
|
output = `#{command} 2>&1`
|
|
56
|
-
exit_code =
|
|
45
|
+
exit_code = $CHILD_STATUS.exitstatus
|
|
46
|
+
|
|
47
|
+
raise CommandExecutionError, "Command failed with exit code #{exit_code}:\n#{output}" unless exit_code.zero?
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
else
|
|
64
|
-
output
|
|
65
|
-
end
|
|
66
|
-
else
|
|
67
|
-
"Command failed with exit code #{exit_code}:\n#{output}"
|
|
68
|
-
end
|
|
49
|
+
CommandResult.new(
|
|
50
|
+
stdout: truncate_output(output),
|
|
51
|
+
stderr: "",
|
|
52
|
+
exit_code: exit_code
|
|
53
|
+
)
|
|
69
54
|
end
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def truncate_output(output)
|
|
58
|
+
lines = output.split("\n")
|
|
59
|
+
return output if lines.length <= 200
|
|
60
|
+
|
|
61
|
+
truncated_output = lines[0..199].join("\n")
|
|
62
|
+
"#{truncated_output}\n\n... (#{lines.length - 200} more lines truncated)"
|
|
72
63
|
end
|
|
73
64
|
end
|
|
74
65
|
end
|
data/lib/rubycode/tools/done.rb
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyCode
|
|
4
4
|
module Tools
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type: "function",
|
|
9
|
-
function: {
|
|
10
|
-
name: "done",
|
|
11
|
-
description: "Call this when you have found the code and are ready to provide your final answer. This signals you are finished exploring.",
|
|
12
|
-
parameters: {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
answer: {
|
|
16
|
-
type: "string",
|
|
17
|
-
description: "Your final answer showing the file, line number, current code, and suggested change"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
required: ["answer"]
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
end
|
|
5
|
+
# Tool for signaling task completion
|
|
6
|
+
class Done < Base
|
|
7
|
+
private
|
|
25
8
|
|
|
26
|
-
def
|
|
27
|
-
# Just return the answer - this is the final response
|
|
9
|
+
def perform(params)
|
|
28
10
|
params["answer"]
|
|
29
11
|
end
|
|
30
12
|
end
|