rubycode 0.1.5 → 0.1.6
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 +11 -1
- data/CHANGELOG.md +28 -0
- data/EXPLORE_TOOL_DESIGN.md +375 -0
- data/config/exploration_prompt.md +56 -0
- data/config/tools/explore.json +22 -0
- data/exe/rubycode_client +66 -7
- data/lib/rubycode/agent_loop.rb +16 -2
- data/lib/rubycode/client/approval_handler.rb +15 -0
- data/lib/rubycode/client.rb +10 -2
- data/lib/rubycode/explorer.rb +61 -0
- data/lib/rubycode/tools/explore.rb +49 -0
- data/lib/rubycode/tools.rb +3 -1
- data/lib/rubycode/version.rb +1 -1
- data/lib/rubycode/views/adapter.rb +6 -0
- data/lib/rubycode/views/cli/auto_approve_disabled.rb +18 -0
- data/lib/rubycode/views/cli/auto_approve_enabled.rb +19 -0
- data/lib/rubycode/views/cli/auto_approve_status.rb +18 -0
- data/lib/rubycode/views/cli/configuration_table.rb +11 -6
- data/lib/rubycode/views/cli/plan_mode_enter.rb +19 -0
- data/lib/rubycode/views/cli/plan_mode_exit.rb +17 -0
- data/lib/rubycode/views/cli.rb +5 -0
- data/lib/rubycode/views.rb +1 -3
- data/lib/rubycode.rb +1 -0
- data/rubycode_cli.rb +66 -7
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00132f001573596d0454dcb6765fc7c5fab4118e99d8755b67d0c7ff9140af81
|
|
4
|
+
data.tar.gz: 9f30d9d75328c0cfab381a02b1d457aaa67eef4f81cc5e535b8df25aae06e05a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 432f02b74ee9bdb44ddddc1d6aa5d8862eb7914081850e393775470dc79fa20d395a8b32d89539f3a280cb5c3c6e03221d75b5b4a266b4ac1ce033eddc1593b0
|
|
7
|
+
data.tar.gz: 35983e256acdd200562446e929f93217dcc9b6bbee6484043bba42ac227ee0e5e382aff1159c3921cb88e6387d89be7521d69d8c6bf4514c89b25fe2b5d29190
|
data/.rubocop.yml
CHANGED
|
@@ -14,11 +14,21 @@ Style/OneClassPerFile:
|
|
|
14
14
|
|
|
15
15
|
# Metrics - reasonable limits for maintainability
|
|
16
16
|
Metrics/ClassLength:
|
|
17
|
-
Max:
|
|
17
|
+
Max: 210
|
|
18
18
|
|
|
19
19
|
Metrics/MethodLength:
|
|
20
20
|
Max: 20
|
|
21
|
+
Exclude:
|
|
22
|
+
- "exe/**/*"
|
|
23
|
+
- "rubycode_cli.rb"
|
|
21
24
|
|
|
22
25
|
Metrics/AbcSize:
|
|
23
26
|
Exclude:
|
|
24
27
|
- "test/**/*"
|
|
28
|
+
- "exe/**/*"
|
|
29
|
+
- "rubycode_cli.rb"
|
|
30
|
+
|
|
31
|
+
Metrics/CyclomaticComplexity:
|
|
32
|
+
Exclude:
|
|
33
|
+
- "exe/**/*"
|
|
34
|
+
- "rubycode_cli.rb"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.6] - 2026-03-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Plan Mode**: Interactive planning workflow with autonomous codebase exploration
|
|
7
|
+
- Type `plan mode` or `plan` to enter planning mode
|
|
8
|
+
- AI explores codebase using the explore tool
|
|
9
|
+
- User approval prompt after exploration: "Do you accept this plan?"
|
|
10
|
+
- Auto-approve automatically enabled for implementation if plan accepted
|
|
11
|
+
- Auto-approve disabled after implementation completes
|
|
12
|
+
- **Auto-Approve Commands**: Manual control over write operation approvals
|
|
13
|
+
- `auto-approve on` / `auto-approve write`: Enable auto-approval with confirmation prompt
|
|
14
|
+
- `auto-approve off`: Disable auto-approval
|
|
15
|
+
- `auto-approve` / `auto-approve status`: Check current auto-approve status
|
|
16
|
+
- **Explore Tool**: Autonomous codebase exploration agent (read-only)
|
|
17
|
+
- Spawns sub-agent with constrained toolset (bash, read, search, web_search, fetch, done)
|
|
18
|
+
- Configurable max iterations (default: 10, max: 15)
|
|
19
|
+
- Structured output format with summary, key files, code flow, and external resources
|
|
20
|
+
- Uses dedicated exploration prompt for systematic investigation
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **CLI Architecture**: Added plan_mode flag to ChatContext struct
|
|
24
|
+
- **Plan Mode Entry Message**: Updated to clearly explain the workflow
|
|
25
|
+
- **Process Flow**: Enhanced process_user_message to handle plan mode and auto-approve lifecycle
|
|
26
|
+
- **rubycode_cli.rb**: Synchronized with exe/rubycode_client to include all new features
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Special Command Handling**: Plan mode now properly handled as special command instead of being sent to AI
|
|
30
|
+
|
|
3
31
|
## [0.1.5] - 2026-03-08
|
|
4
32
|
|
|
5
33
|
### Added
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Explore Tool Design for RubyCode
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
An intelligent codebase exploration tool that combines file discovery, content search, and automatic summarization.
|
|
5
|
+
|
|
6
|
+
## How Claude Code's Explore Works
|
|
7
|
+
|
|
8
|
+
Claude Code's Explore agent:
|
|
9
|
+
1. Takes a natural language query (e.g., "how does authentication work?")
|
|
10
|
+
2. Automatically uses multiple tools (Glob, Grep, Read) in sequence
|
|
11
|
+
3. Follows breadcrumbs and references
|
|
12
|
+
4. Builds a mental map of the codebase
|
|
13
|
+
5. Returns a comprehensive answer
|
|
14
|
+
|
|
15
|
+
## Implementation Approach for RubyCode
|
|
16
|
+
|
|
17
|
+
### Option 1: Single Explore Tool (Recommended)
|
|
18
|
+
Create a new `explore` tool that acts as an intelligent wrapper.
|
|
19
|
+
|
|
20
|
+
**Tool Definition** (`config/tools/explore.json`):
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"type": "function",
|
|
24
|
+
"function": {
|
|
25
|
+
"name": "explore",
|
|
26
|
+
"description": "Intelligently explore the codebase to answer questions. Automatically finds relevant files, searches content, and builds context. Use this for questions like 'how does X work?', 'where is Y implemented?', 'what files handle Z?'",
|
|
27
|
+
"parameters": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"query": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Natural language question or exploration goal (e.g., 'how does user authentication work?')"
|
|
33
|
+
},
|
|
34
|
+
"thoroughness": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["quick", "medium", "thorough"],
|
|
37
|
+
"description": "How deep to explore. 'quick' = 1-2 passes, 'medium' = 3-5 passes, 'thorough' = 5-10 passes. Default: 'medium'"
|
|
38
|
+
},
|
|
39
|
+
"focus_paths": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {"type": "string"},
|
|
42
|
+
"description": "Optional array of directory paths to focus on (e.g., ['app/models', 'lib/auth'])"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": ["query"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Implementation** (`lib/rubycode/tools/explore.rb`):
|
|
52
|
+
```ruby
|
|
53
|
+
module RubyCode
|
|
54
|
+
module Tools
|
|
55
|
+
class Explore < Base
|
|
56
|
+
def execute(params)
|
|
57
|
+
query = params["query"]
|
|
58
|
+
thoroughness = params["thoroughness"] || "medium"
|
|
59
|
+
focus_paths = params["focus_paths"] || ["."]
|
|
60
|
+
|
|
61
|
+
# Create a mini-agent that explores
|
|
62
|
+
explorer = Explorer.new(
|
|
63
|
+
root_path: context[:root_path],
|
|
64
|
+
query: query,
|
|
65
|
+
thoroughness: thoroughness,
|
|
66
|
+
focus_paths: focus_paths
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result = explorer.run
|
|
70
|
+
|
|
71
|
+
CommandResult.new(
|
|
72
|
+
success: true,
|
|
73
|
+
output: result.summary,
|
|
74
|
+
metadata: {
|
|
75
|
+
files_examined: result.files_examined,
|
|
76
|
+
patterns_found: result.patterns_found
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Explorer Engine** (`lib/rubycode/explorer.rb`):
|
|
86
|
+
```ruby
|
|
87
|
+
module RubyCode
|
|
88
|
+
class Explorer
|
|
89
|
+
MAX_ITERATIONS = {
|
|
90
|
+
"quick" => 3,
|
|
91
|
+
"medium" => 7,
|
|
92
|
+
"thorough" => 15
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
def initialize(root_path:, query:, thoroughness:, focus_paths:)
|
|
96
|
+
@root_path = root_path
|
|
97
|
+
@query = query
|
|
98
|
+
@max_iterations = MAX_ITERATIONS[thoroughness]
|
|
99
|
+
@focus_paths = focus_paths
|
|
100
|
+
@examined_files = []
|
|
101
|
+
@findings = []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run
|
|
105
|
+
# Phase 1: Keyword extraction
|
|
106
|
+
keywords = extract_keywords(@query)
|
|
107
|
+
|
|
108
|
+
# Phase 2: File discovery
|
|
109
|
+
candidate_files = discover_files(keywords)
|
|
110
|
+
|
|
111
|
+
# Phase 3: Content analysis
|
|
112
|
+
analyze_content(candidate_files, keywords)
|
|
113
|
+
|
|
114
|
+
# Phase 4: Follow references
|
|
115
|
+
follow_references(keywords)
|
|
116
|
+
|
|
117
|
+
# Phase 5: Summarize findings
|
|
118
|
+
summarize
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def extract_keywords(query)
|
|
124
|
+
# Extract relevant keywords from query
|
|
125
|
+
# Remove stop words, extract technical terms
|
|
126
|
+
words = query.downcase.split(/\s+/)
|
|
127
|
+
stop_words = %w[how does what where is the a an in on at to for of with]
|
|
128
|
+
words - stop_words
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def discover_files(keywords)
|
|
132
|
+
files = []
|
|
133
|
+
|
|
134
|
+
# Try file name patterns
|
|
135
|
+
keywords.each do |keyword|
|
|
136
|
+
# Try exact match
|
|
137
|
+
files += glob_search("**/*#{keyword}*")
|
|
138
|
+
|
|
139
|
+
# Try snake_case variant
|
|
140
|
+
snake = keyword.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
|
|
141
|
+
files += glob_search("**/*#{snake}*")
|
|
142
|
+
|
|
143
|
+
# Try camel_case variant
|
|
144
|
+
camel = keyword.split('_').map(&:capitalize).join
|
|
145
|
+
files += glob_search("**/*#{camel}*")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
files.uniq
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def analyze_content(files, keywords)
|
|
152
|
+
files.first(20).each do |file|
|
|
153
|
+
next if @examined_files.include?(file)
|
|
154
|
+
|
|
155
|
+
content = read_file(file)
|
|
156
|
+
next unless content
|
|
157
|
+
|
|
158
|
+
# Check if file is relevant
|
|
159
|
+
relevance_score = calculate_relevance(content, keywords)
|
|
160
|
+
|
|
161
|
+
if relevance_score > 0.3
|
|
162
|
+
@findings << {
|
|
163
|
+
file: file,
|
|
164
|
+
score: relevance_score,
|
|
165
|
+
snippets: extract_relevant_snippets(content, keywords)
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@examined_files << file
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def follow_references(keywords)
|
|
174
|
+
# Look for requires, includes, imports
|
|
175
|
+
@findings.each do |finding|
|
|
176
|
+
content = read_file(finding[:file])
|
|
177
|
+
|
|
178
|
+
# Extract references (require, include, etc.)
|
|
179
|
+
references = extract_references(content)
|
|
180
|
+
|
|
181
|
+
# Recursively explore referenced files
|
|
182
|
+
references.each do |ref|
|
|
183
|
+
ref_file = resolve_reference(ref)
|
|
184
|
+
next unless ref_file
|
|
185
|
+
|
|
186
|
+
analyze_content([ref_file], keywords)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def summarize
|
|
192
|
+
# Build a structured summary
|
|
193
|
+
summary = "# Exploration Results for: #{@query}\n\n"
|
|
194
|
+
summary += "Examined #{@examined_files.length} files\n\n"
|
|
195
|
+
|
|
196
|
+
# Group findings by relevance
|
|
197
|
+
top_findings = @findings.sort_by { |f| -f[:score] }.first(10)
|
|
198
|
+
|
|
199
|
+
summary += "## Most Relevant Files:\n\n"
|
|
200
|
+
top_findings.each do |finding|
|
|
201
|
+
summary += "**#{finding[:file]}** (relevance: #{(finding[:score] * 100).round}%)\n"
|
|
202
|
+
finding[:snippets].each do |snippet|
|
|
203
|
+
summary += " - Line #{snippet[:line]}: #{snippet[:text]}\n"
|
|
204
|
+
end
|
|
205
|
+
summary += "\n"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
ExplorerResult.new(
|
|
209
|
+
summary: summary,
|
|
210
|
+
files_examined: @examined_files.length,
|
|
211
|
+
patterns_found: @findings.length
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Helper methods
|
|
216
|
+
def glob_search(pattern)
|
|
217
|
+
Dir.glob(File.join(@root_path, pattern)).select { |f| File.file?(f) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def read_file(path)
|
|
221
|
+
File.read(path) rescue nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def calculate_relevance(content, keywords)
|
|
225
|
+
matches = keywords.sum { |kw| content.scan(/#{Regexp.escape(kw)}/i).length }
|
|
226
|
+
matches.to_f / (content.length / 100.0)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def extract_relevant_snippets(content, keywords)
|
|
230
|
+
snippets = []
|
|
231
|
+
content.lines.each_with_index do |line, idx|
|
|
232
|
+
if keywords.any? { |kw| line.match?(/#{Regexp.escape(kw)}/i) }
|
|
233
|
+
snippets << { line: idx + 1, text: line.strip }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
snippets.first(5)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def extract_references(content)
|
|
240
|
+
references = []
|
|
241
|
+
|
|
242
|
+
# Ruby requires
|
|
243
|
+
content.scan(/require\s+['"](.+?)['"]/) { references << $1 }
|
|
244
|
+
content.scan(/require_relative\s+['"](.+?)['"]/) { references << $1 }
|
|
245
|
+
|
|
246
|
+
references
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def resolve_reference(ref)
|
|
250
|
+
# Try to find the actual file
|
|
251
|
+
possible_paths = [
|
|
252
|
+
"#{ref}.rb",
|
|
253
|
+
"lib/#{ref}.rb",
|
|
254
|
+
"app/#{ref}.rb"
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
possible_paths.find { |p| File.exist?(File.join(@root_path, p)) }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
ExplorerResult = Struct.new(:summary, :files_examined, :patterns_found, keyword_init: true)
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Option 2: Sub-Agent Approach (More Advanced)
|
|
266
|
+
|
|
267
|
+
Create a separate mini-agent that has access to a limited set of tools and runs autonomously:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
class ExploreAgent
|
|
271
|
+
def initialize(query:, root_path:, thoroughness:)
|
|
272
|
+
@query = query
|
|
273
|
+
@root_path = root_path
|
|
274
|
+
@memory = Memory.new
|
|
275
|
+
@adapter = RubyCode.configuration.adapter
|
|
276
|
+
@max_iterations = thoroughness_to_iterations(thoroughness)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def run
|
|
280
|
+
# Give the sub-agent a focused prompt
|
|
281
|
+
prompt = build_exploration_prompt(@query)
|
|
282
|
+
@memory.add_message(role: "user", content: prompt)
|
|
283
|
+
|
|
284
|
+
# Run mini agent loop with limited tools
|
|
285
|
+
agent_loop = AgentLoop.new(
|
|
286
|
+
adapter: @adapter,
|
|
287
|
+
memory: @memory,
|
|
288
|
+
config: exploration_config,
|
|
289
|
+
system_prompt: exploration_system_prompt,
|
|
290
|
+
max_iterations: @max_iterations
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
agent_loop.run
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def exploration_config
|
|
299
|
+
# Limited config for sub-agent
|
|
300
|
+
config = RubyCode::Configuration.new
|
|
301
|
+
config.root_path = @root_path
|
|
302
|
+
config.memory_window = 5 # Smaller window
|
|
303
|
+
config
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def exploration_system_prompt
|
|
307
|
+
<<~PROMPT
|
|
308
|
+
You are an expert codebase explorer. Your goal is to thoroughly explore
|
|
309
|
+
the codebase to answer the user's question. You have access to these tools:
|
|
310
|
+
|
|
311
|
+
- bash: Run ls, find, tree commands to discover files
|
|
312
|
+
- search: Search file contents for patterns
|
|
313
|
+
- read: Read specific files
|
|
314
|
+
- done: Finish with your findings
|
|
315
|
+
|
|
316
|
+
Be thorough but efficient. Follow these steps:
|
|
317
|
+
1. Identify key terms from the question
|
|
318
|
+
2. Search for relevant files and code
|
|
319
|
+
3. Read the most promising files
|
|
320
|
+
4. Follow references and imports
|
|
321
|
+
5. Summarize your findings
|
|
322
|
+
|
|
323
|
+
Use 'done' when you have a comprehensive answer.
|
|
324
|
+
PROMPT
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def build_exploration_prompt(query)
|
|
328
|
+
"Explore the codebase to answer this question: #{query}\n\n" \
|
|
329
|
+
"Provide a comprehensive answer with file references and code snippets."
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Comparison
|
|
335
|
+
|
|
336
|
+
| Approach | Pros | Cons |
|
|
337
|
+
|----------|------|------|
|
|
338
|
+
| **Single Tool** | Simple, fast, deterministic | Less flexible, hardcoded logic |
|
|
339
|
+
| **Sub-Agent** | Intelligent, adaptable, can reason | Higher token cost, slower |
|
|
340
|
+
|
|
341
|
+
## Recommendation
|
|
342
|
+
|
|
343
|
+
**Start with Option 1 (Single Tool)**, then evolve to Option 2 if needed:
|
|
344
|
+
|
|
345
|
+
1. **Phase 1**: Implement basic explore tool with keyword extraction
|
|
346
|
+
2. **Phase 2**: Add pattern matching and file scoring
|
|
347
|
+
3. **Phase 3**: Add reference following
|
|
348
|
+
4. **Phase 4**: Consider sub-agent for complex queries
|
|
349
|
+
|
|
350
|
+
## Token Efficiency
|
|
351
|
+
|
|
352
|
+
The sub-agent approach uses more tokens but provides better results:
|
|
353
|
+
|
|
354
|
+
- **Single tool**: ~500-1000 tokens per exploration
|
|
355
|
+
- **Sub-agent**: ~5000-15000 tokens per exploration
|
|
356
|
+
|
|
357
|
+
You could make it configurable:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# Quick exploration (single tool)
|
|
361
|
+
explore(query: "find authentication", mode: "fast")
|
|
362
|
+
|
|
363
|
+
# Deep exploration (sub-agent)
|
|
364
|
+
explore(query: "how does authentication work?", mode: "deep")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Next Steps
|
|
368
|
+
|
|
369
|
+
1. Create `lib/rubycode/tools/explore.rb`
|
|
370
|
+
2. Create `lib/rubycode/explorer.rb`
|
|
371
|
+
3. Create `config/tools/explore.json`
|
|
372
|
+
4. Add tests in `test/test_explore_tool.rb`
|
|
373
|
+
5. Update README with explore tool documentation
|
|
374
|
+
|
|
375
|
+
Would you like me to implement the basic version (Option 1)?
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Codebase Explorer
|
|
2
|
+
|
|
3
|
+
You are an expert codebase explorer. Your goal is to thoroughly explore the codebase to answer the user's question.
|
|
4
|
+
|
|
5
|
+
**IMPORTANT: This is READ-ONLY exploration. You cannot modify any files.**
|
|
6
|
+
|
|
7
|
+
## Available Tools
|
|
8
|
+
|
|
9
|
+
- **bash**: Run ls, find, tree, grep commands to discover files (read-only commands only)
|
|
10
|
+
- **read**: Read file contents with line numbers
|
|
11
|
+
- **search**: Search file contents for patterns using regex
|
|
12
|
+
- **web_search**: Search the internet for documentation, examples, best practices
|
|
13
|
+
- **fetch**: Fetch documentation from URLs
|
|
14
|
+
- **done**: Signal completion with your findings
|
|
15
|
+
|
|
16
|
+
## Exploration Strategy
|
|
17
|
+
|
|
18
|
+
Follow these steps for thorough exploration:
|
|
19
|
+
|
|
20
|
+
1. **Extract keywords** from the user's query
|
|
21
|
+
2. **Discover files** using bash (find, ls, grep) and search tools
|
|
22
|
+
3. **Read promising files** to understand implementation
|
|
23
|
+
4. **Look up documentation** if needed using web_search or fetch
|
|
24
|
+
5. **Follow references** and imports to related code
|
|
25
|
+
6. **Synthesize answer** and call done with structured summary
|
|
26
|
+
|
|
27
|
+
## Output Format
|
|
28
|
+
|
|
29
|
+
When you finish exploring, use the 'done' tool with this structured format:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
## Summary
|
|
33
|
+
[1-3 sentence answer to the user's question]
|
|
34
|
+
|
|
35
|
+
## Key Files
|
|
36
|
+
- path/to/file.rb: [brief description of what this file does]
|
|
37
|
+
- path/to/other.rb: [brief description of what this file does]
|
|
38
|
+
|
|
39
|
+
## Code Flow
|
|
40
|
+
[If applicable, explain how the components interact and data flows through the system]
|
|
41
|
+
|
|
42
|
+
## External Resources
|
|
43
|
+
[If you used web_search/fetch, list relevant documentation URLs with brief descriptions]
|
|
44
|
+
|
|
45
|
+
## Additional Notes
|
|
46
|
+
[Any important caveats, patterns, or recommendations]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Important Reminders
|
|
50
|
+
|
|
51
|
+
- **READ-ONLY MODE**: You cannot write, update, or modify any files
|
|
52
|
+
- Be thorough but efficient with your iterations
|
|
53
|
+
- Actually read the files you identify as important
|
|
54
|
+
- If stuck, try web search for documentation
|
|
55
|
+
- Focus on answering the user's specific question, not general exploration
|
|
56
|
+
- Your findings will help the user plan their implementation
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "explore",
|
|
5
|
+
"description": "Autonomously explore the codebase to answer questions about architecture, implementation, and code flow. READ-ONLY exploration - no files will be modified. The agent will use bash, read, search, web_search, and fetch tools to thoroughly investigate the codebase and provide findings. Use this for questions like 'how does authentication work?', 'where is user management implemented?', 'what files handle routing?'",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"query": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The exploration question or goal (e.g., 'how does user authentication work in this codebase?')"
|
|
12
|
+
},
|
|
13
|
+
"max_iterations": {
|
|
14
|
+
"type": "integer",
|
|
15
|
+
"description": "Maximum number of exploration iterations (default: 10, max: 15). Higher values allow more thorough exploration but use more tokens.",
|
|
16
|
+
"default": 10
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": ["query"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
data/exe/rubycode_client
CHANGED
|
@@ -206,7 +206,7 @@ if adapter_info[:requires_key]
|
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
-
ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode)
|
|
209
|
+
ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode, :plan_mode)
|
|
210
210
|
|
|
211
211
|
def run_chat_loop(context)
|
|
212
212
|
loop do
|
|
@@ -215,7 +215,7 @@ def run_chat_loop(context)
|
|
|
215
215
|
|
|
216
216
|
next if handle_special_command(user_input, context)
|
|
217
217
|
|
|
218
|
-
process_user_message(context.client, user_input)
|
|
218
|
+
process_user_message(context.client, user_input, context)
|
|
219
219
|
end
|
|
220
220
|
end
|
|
221
221
|
|
|
@@ -237,6 +237,32 @@ def handle_special_command(input, context)
|
|
|
237
237
|
when "config"
|
|
238
238
|
show_config_and_reconfigure(context)
|
|
239
239
|
true
|
|
240
|
+
when "plan", "plan mode"
|
|
241
|
+
puts RubyCode::Views::Cli::PlanModeEnter.build
|
|
242
|
+
context.plan_mode = true
|
|
243
|
+
true
|
|
244
|
+
when "auto-approve on", "auto-approve write"
|
|
245
|
+
confirmed = context.prompt.yes?(
|
|
246
|
+
"⚠ Enable auto-approve for write/update operations?\n" \
|
|
247
|
+
"Files will be modified without confirmation.",
|
|
248
|
+
default: false
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if confirmed
|
|
252
|
+
context.client.approval_handler.enable_auto_approve_write
|
|
253
|
+
puts RubyCode::Views::Cli::AutoApproveEnabled.build
|
|
254
|
+
else
|
|
255
|
+
puts "\nAuto-approve NOT enabled.\n\n"
|
|
256
|
+
end
|
|
257
|
+
true
|
|
258
|
+
when "auto-approve off"
|
|
259
|
+
context.client.approval_handler.disable_auto_approve_write
|
|
260
|
+
puts RubyCode::Views::Cli::AutoApproveDisabled.build
|
|
261
|
+
true
|
|
262
|
+
when "auto-approve", "auto-approve status"
|
|
263
|
+
status = context.client.approval_handler.auto_approve_write_enabled
|
|
264
|
+
puts RubyCode::Views::Cli::AutoApproveStatus.build(enabled: status)
|
|
265
|
+
true
|
|
240
266
|
else
|
|
241
267
|
false
|
|
242
268
|
end
|
|
@@ -247,7 +273,8 @@ def show_config_and_reconfigure(context)
|
|
|
247
273
|
adapter: context.adapter,
|
|
248
274
|
model: context.model,
|
|
249
275
|
directory: context.full_path,
|
|
250
|
-
debug_mode: context.debug_mode
|
|
276
|
+
debug_mode: context.debug_mode,
|
|
277
|
+
auto_approve: context.client.approval_handler.auto_approve_write_enabled
|
|
251
278
|
)
|
|
252
279
|
|
|
253
280
|
return unless context.prompt.yes?(I18n.t("rubycode.setup.reconfigure"), default: false)
|
|
@@ -256,9 +283,40 @@ def show_config_and_reconfigure(context)
|
|
|
256
283
|
setup_wizard(context.prompt)
|
|
257
284
|
end
|
|
258
285
|
|
|
259
|
-
def process_user_message(client, user_input)
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
def process_user_message(client, user_input, context)
|
|
287
|
+
if context.plan_mode
|
|
288
|
+
# In plan mode - use explore tool then ask for approval
|
|
289
|
+
response = client.ask(prompt: "Use the explore tool with this query: #{user_input}")
|
|
290
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
291
|
+
|
|
292
|
+
# Ask user if they accept the plan
|
|
293
|
+
accept_plan = context.prompt.yes?("\nDo you accept this plan and want to proceed with implementation?",
|
|
294
|
+
default: true)
|
|
295
|
+
|
|
296
|
+
if accept_plan
|
|
297
|
+
puts "\n✓ Plan accepted. Auto-approve enabled for implementation.\n"
|
|
298
|
+
# Enable auto-approve for implementation
|
|
299
|
+
client.approval_handler.enable_auto_approve_write
|
|
300
|
+
|
|
301
|
+
# Ask for implementation prompt
|
|
302
|
+
impl_prompt = context.prompt.ask("Describe what you want to implement:")
|
|
303
|
+
response = client.ask(prompt: impl_prompt)
|
|
304
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
305
|
+
|
|
306
|
+
# Disable auto-approve after implementation
|
|
307
|
+
client.approval_handler.disable_auto_approve_write
|
|
308
|
+
else
|
|
309
|
+
puts "\n✗ Plan rejected. Returning to normal mode.\n"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Exit plan mode after exploration
|
|
313
|
+
context.plan_mode = false
|
|
314
|
+
puts RubyCode::Views::Cli::PlanModeExit.build
|
|
315
|
+
else
|
|
316
|
+
# Normal mode
|
|
317
|
+
response = client.ask(prompt: user_input)
|
|
318
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
319
|
+
end
|
|
262
320
|
rescue Interrupt
|
|
263
321
|
puts RubyCode::Views::Cli::InterruptMessage.build
|
|
264
322
|
rescue StandardError => e
|
|
@@ -270,5 +328,6 @@ client = RubyCode::Client.new(tty_prompt: prompt)
|
|
|
270
328
|
puts RubyCode::Views::Cli::ReadyMessage.build
|
|
271
329
|
|
|
272
330
|
debug_mode = false # Debug mode not yet implemented
|
|
273
|
-
|
|
331
|
+
plan_mode = false # Plan mode flag
|
|
332
|
+
context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode, plan_mode)
|
|
274
333
|
run_chat_loop(context)
|
data/lib/rubycode/agent_loop.rb
CHANGED
|
@@ -11,6 +11,8 @@ module RubyCode
|
|
|
11
11
|
MAX_TOOL_CALLS = 50
|
|
12
12
|
MAX_CONSECUTIVE_RATE_LIMIT_ERRORS = 3
|
|
13
13
|
|
|
14
|
+
attr_reader :approval_handler
|
|
15
|
+
|
|
14
16
|
def initialize(adapter:, memory:, config:, system_prompt:, options: {})
|
|
15
17
|
@adapter = adapter
|
|
16
18
|
@memory = memory
|
|
@@ -18,9 +20,11 @@ module RubyCode
|
|
|
18
20
|
@system_prompt = system_prompt
|
|
19
21
|
@read_files = options[:read_files]
|
|
20
22
|
@tty_prompt = options[:tty_prompt]
|
|
23
|
+
@options = options
|
|
21
24
|
@response_handler = Client::ResponseHandler.new(memory: @memory, config: @config)
|
|
22
25
|
@display_formatter = Client::DisplayFormatter.new(config: @config)
|
|
23
|
-
@approval_handler =
|
|
26
|
+
@approval_handler = options[:approval_handler] ||
|
|
27
|
+
Client::ApprovalHandler.new(tty_prompt: @tty_prompt, config: @config)
|
|
24
28
|
@consecutive_rate_limit_errors = 0
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -101,10 +105,20 @@ module RubyCode
|
|
|
101
105
|
window_size: @config.memory_window,
|
|
102
106
|
prune_tool_results: @config.prune_tool_results
|
|
103
107
|
)
|
|
108
|
+
|
|
109
|
+
# Filter tools if allowed_tools is specified
|
|
110
|
+
tools_to_send = if @options[:allowed_tools]
|
|
111
|
+
Tools.definitions.select do |t|
|
|
112
|
+
@options[:allowed_tools].include?(t[:function][:name])
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
Tools.definitions
|
|
116
|
+
end
|
|
117
|
+
|
|
104
118
|
@adapter.generate(
|
|
105
119
|
messages: messages,
|
|
106
120
|
system: @system_prompt,
|
|
107
|
-
tools:
|
|
121
|
+
tools: tools_to_send
|
|
108
122
|
)
|
|
109
123
|
end
|
|
110
124
|
|
|
@@ -4,9 +4,20 @@ module RubyCode
|
|
|
4
4
|
class Client
|
|
5
5
|
# Handles user approval prompts for tools
|
|
6
6
|
class ApprovalHandler
|
|
7
|
+
attr_reader :auto_approve_write_enabled
|
|
8
|
+
|
|
7
9
|
def initialize(tty_prompt:, config:)
|
|
8
10
|
@prompt = tty_prompt
|
|
9
11
|
@config = config
|
|
12
|
+
@auto_approve_write_enabled = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enable_auto_approve_write
|
|
16
|
+
@auto_approve_write_enabled = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def disable_auto_approve_write
|
|
20
|
+
@auto_approve_write_enabled = false
|
|
10
21
|
end
|
|
11
22
|
|
|
12
23
|
def request_bash_approval(command, base_command, safe_commands)
|
|
@@ -25,6 +36,8 @@ module RubyCode
|
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
def request_write_approval(file_path, content)
|
|
39
|
+
return true if @auto_approve_write_enabled
|
|
40
|
+
|
|
28
41
|
display = Views::WriteApproval.build(
|
|
29
42
|
file_path: file_path,
|
|
30
43
|
content: content
|
|
@@ -39,6 +52,8 @@ module RubyCode
|
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
def request_update_approval(file_path, old_string, new_string)
|
|
55
|
+
return true if @auto_approve_write_enabled
|
|
56
|
+
|
|
42
57
|
display = Views::UpdateApproval.build(
|
|
43
58
|
file_path: file_path,
|
|
44
59
|
old_string: old_string,
|
data/lib/rubycode/client.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "set"
|
|
|
5
5
|
module RubyCode
|
|
6
6
|
# Main client that provides the public API for the agent
|
|
7
7
|
class Client
|
|
8
|
-
attr_reader :memory
|
|
8
|
+
attr_reader :memory, :approval_handler
|
|
9
9
|
|
|
10
10
|
def initialize(tty_prompt: nil)
|
|
11
11
|
@config = RubyCode.config
|
|
@@ -14,6 +14,10 @@ module RubyCode
|
|
|
14
14
|
@memory.clear # Clear memory at start of each session to prevent payload size issues
|
|
15
15
|
@read_files = Set.new
|
|
16
16
|
@tty_prompt = tty_prompt
|
|
17
|
+
@approval_handler = Client::ApprovalHandler.new(
|
|
18
|
+
tty_prompt: @tty_prompt,
|
|
19
|
+
config: @config
|
|
20
|
+
)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def ask(prompt:)
|
|
@@ -25,7 +29,11 @@ module RubyCode
|
|
|
25
29
|
memory: @memory,
|
|
26
30
|
config: @config,
|
|
27
31
|
system_prompt: system_prompt,
|
|
28
|
-
options: {
|
|
32
|
+
options: {
|
|
33
|
+
read_files: @read_files,
|
|
34
|
+
tty_prompt: @tty_prompt,
|
|
35
|
+
approval_handler: @approval_handler
|
|
36
|
+
}
|
|
29
37
|
).run
|
|
30
38
|
end
|
|
31
39
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCode
|
|
4
|
+
# Explorer engine that spawns a sub-agent for autonomous codebase exploration
|
|
5
|
+
class Explorer
|
|
6
|
+
MAX_ITERATIONS = 15
|
|
7
|
+
ALLOWED_TOOLS = %w[bash read search web_search fetch done].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(adapter:, config:, query:, max_iterations: 10)
|
|
10
|
+
@adapter = adapter
|
|
11
|
+
@config = config
|
|
12
|
+
@query = query
|
|
13
|
+
@max_iterations = max_iterations.clamp(1, MAX_ITERATIONS)
|
|
14
|
+
@memory = Memory.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def explore
|
|
18
|
+
# Add user query to memory
|
|
19
|
+
@memory.add_message(role: "user", content: @query)
|
|
20
|
+
|
|
21
|
+
# Build and run constrained agent loop
|
|
22
|
+
agent_loop = build_constrained_loop
|
|
23
|
+
|
|
24
|
+
puts "\n🔍 Exploring codebase...\n"
|
|
25
|
+
result = agent_loop.run
|
|
26
|
+
|
|
27
|
+
puts "\n✓ Exploration complete\n\n"
|
|
28
|
+
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_constrained_loop
|
|
35
|
+
# Load exploration prompt
|
|
36
|
+
system_prompt = File.read(File.join(__dir__, "../../config/exploration_prompt.md"))
|
|
37
|
+
|
|
38
|
+
AgentLoop.new(
|
|
39
|
+
adapter: @adapter,
|
|
40
|
+
memory: @memory,
|
|
41
|
+
config: @config,
|
|
42
|
+
system_prompt: system_prompt,
|
|
43
|
+
options: {
|
|
44
|
+
max_iterations: @max_iterations,
|
|
45
|
+
allowed_tools: ALLOWED_TOOLS,
|
|
46
|
+
read_files: Set.new,
|
|
47
|
+
tty_prompt: TTY::Prompt.new,
|
|
48
|
+
approval_handler: create_explorer_approval_handler
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_explorer_approval_handler
|
|
54
|
+
# Create approval handler for web_search and fetch (bash is still approved per safe-list)
|
|
55
|
+
Client::ApprovalHandler.new(
|
|
56
|
+
tty_prompt: TTY::Prompt.new,
|
|
57
|
+
config: @config
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCode
|
|
4
|
+
module Tools
|
|
5
|
+
# Explore tool - spawns a sub-agent for autonomous codebase exploration
|
|
6
|
+
class Explore < Base
|
|
7
|
+
def perform(params)
|
|
8
|
+
query = params["query"]
|
|
9
|
+
max_iterations = params["max_iterations"] || 10
|
|
10
|
+
|
|
11
|
+
# Build a fresh adapter for the sub-agent
|
|
12
|
+
adapter = build_adapter
|
|
13
|
+
|
|
14
|
+
# Create explorer and run
|
|
15
|
+
explorer = Explorer.new(
|
|
16
|
+
adapter: adapter,
|
|
17
|
+
config: context[:config] || RubyCode.config,
|
|
18
|
+
query: query,
|
|
19
|
+
max_iterations: max_iterations
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
explorer.explore
|
|
23
|
+
|
|
24
|
+
# Return the exploration result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_adapter
|
|
30
|
+
config = context[:config] || RubyCode.config
|
|
31
|
+
|
|
32
|
+
case config.adapter
|
|
33
|
+
when :ollama
|
|
34
|
+
Adapters::Ollama.new(config)
|
|
35
|
+
when :openrouter
|
|
36
|
+
Adapters::Openrouter.new(config)
|
|
37
|
+
when :deepseek
|
|
38
|
+
Adapters::Deepseek.new(config)
|
|
39
|
+
when :gemini
|
|
40
|
+
Adapters::Gemini.new(config)
|
|
41
|
+
when :openai
|
|
42
|
+
Adapters::Openai.new(config)
|
|
43
|
+
else
|
|
44
|
+
raise ToolError, "Unknown adapter: #{config.adapter}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/rubycode/tools.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "tools/update"
|
|
|
9
9
|
require_relative "tools/done"
|
|
10
10
|
require_relative "tools/web_search"
|
|
11
11
|
require_relative "tools/fetch"
|
|
12
|
+
require_relative "tools/explore"
|
|
12
13
|
|
|
13
14
|
module RubyCode
|
|
14
15
|
# Collection of available tools for the AI agent
|
|
@@ -22,7 +23,8 @@ module RubyCode
|
|
|
22
23
|
Update,
|
|
23
24
|
Done,
|
|
24
25
|
WebSearch,
|
|
25
|
-
Fetch
|
|
26
|
+
Fetch,
|
|
27
|
+
Explore
|
|
26
28
|
].freeze
|
|
27
29
|
|
|
28
30
|
def self.definitions
|
data/lib/rubycode/version.rb
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Cli
|
|
8
|
+
# Builds auto-approve disabled message
|
|
9
|
+
class AutoApproveDisabled
|
|
10
|
+
def self.build
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
"\n#{pastel.green("✓")} Auto-approve disabled\n" \
|
|
13
|
+
"You will be prompted before file operations.\n\n"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Cli
|
|
8
|
+
# Builds auto-approve enabled message
|
|
9
|
+
class AutoApproveEnabled
|
|
10
|
+
def self.build
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
"\n#{pastel.green("✓")} Auto-approve enabled for write and update operations\n" \
|
|
13
|
+
"#{pastel.yellow("⚠")} Files will be modified without confirmation.\n" \
|
|
14
|
+
"Use 'auto-approve off' to disable.\n\n"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Cli
|
|
8
|
+
# Builds auto-approve status message
|
|
9
|
+
class AutoApproveStatus
|
|
10
|
+
def self.build(enabled:)
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
status = enabled ? pastel.green("ENABLED") : pastel.dim("DISABLED")
|
|
13
|
+
"\nAuto-approve status: #{status}\n\n"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -8,16 +8,21 @@ module RubyCode
|
|
|
8
8
|
module Cli
|
|
9
9
|
# Builds configuration table display
|
|
10
10
|
class ConfigurationTable
|
|
11
|
-
def self.build(directory:, model:, adapter: :ollama)
|
|
11
|
+
def self.build(directory:, model:, adapter: :ollama, debug_mode: false, auto_approve: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
12
|
pastel = Pastel.new
|
|
13
13
|
|
|
14
|
+
rows = [
|
|
15
|
+
["Adapter", adapter.to_s.capitalize],
|
|
16
|
+
["Model", model],
|
|
17
|
+
["Directory", directory]
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Add auto-approve status if enabled
|
|
21
|
+
rows << ["Auto-approve", "#{pastel.green("ENABLED")} #{pastel.yellow("⚠")}"] if auto_approve
|
|
22
|
+
|
|
14
23
|
table = TTY::Table.new(
|
|
15
24
|
header: [pastel.bold("Setting"), pastel.bold("Value")],
|
|
16
|
-
rows:
|
|
17
|
-
["Adapter", adapter.to_s.capitalize],
|
|
18
|
-
["Model", model],
|
|
19
|
-
["Directory", directory]
|
|
20
|
-
]
|
|
25
|
+
rows: rows
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
"\n#{table.render(:unicode, padding: [0, 1])}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Cli
|
|
8
|
+
# Builds plan mode entry message
|
|
9
|
+
class PlanModeEnter
|
|
10
|
+
def self.build
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
"\n#{pastel.cyan("📋")} Entering Plan Mode\n" \
|
|
13
|
+
"Next: Describe what you want to explore and implement.\n" \
|
|
14
|
+
"The AI will explore the codebase, present a plan, and ask for your approval.\n\n"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
module Views
|
|
7
|
+
module Cli
|
|
8
|
+
# Builds plan mode exit message
|
|
9
|
+
class PlanModeExit
|
|
10
|
+
def self.build
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
"\n#{pastel.green("✓")} Plan Mode complete. Returning to normal mode.\n\n"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/rubycode/views/cli.rb
CHANGED
|
@@ -14,3 +14,8 @@ require_relative "cli/first_time_setup"
|
|
|
14
14
|
require_relative "cli/api_key_missing"
|
|
15
15
|
require_relative "cli/config_saved"
|
|
16
16
|
require_relative "cli/restart_message"
|
|
17
|
+
require_relative "cli/plan_mode_enter"
|
|
18
|
+
require_relative "cli/plan_mode_exit"
|
|
19
|
+
require_relative "cli/auto_approve_enabled"
|
|
20
|
+
require_relative "cli/auto_approve_disabled"
|
|
21
|
+
require_relative "cli/auto_approve_status"
|
data/lib/rubycode/views.rb
CHANGED
|
@@ -8,10 +8,8 @@ require_relative "views/update_approval"
|
|
|
8
8
|
require_relative "views/skip_notification"
|
|
9
9
|
require_relative "views/web_search_approval"
|
|
10
10
|
require_relative "views/cli"
|
|
11
|
+
require_relative "views/adapter"
|
|
11
12
|
require_relative "views/agent_loop"
|
|
12
13
|
require_relative "views/formatter"
|
|
13
14
|
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
15
|
require_relative "views/agent_loop/token_summary"
|
data/lib/rubycode.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "rubycode/adapters/openrouter"
|
|
|
20
20
|
require_relative "rubycode/adapters/deepseek"
|
|
21
21
|
require_relative "rubycode/adapters/gemini"
|
|
22
22
|
require_relative "rubycode/adapters/openai"
|
|
23
|
+
require_relative "rubycode/explorer"
|
|
23
24
|
require_relative "rubycode/tools"
|
|
24
25
|
require_relative "rubycode/agent_loop"
|
|
25
26
|
require_relative "rubycode/client"
|
data/rubycode_cli.rb
CHANGED
|
@@ -206,7 +206,7 @@ if adapter_info[:requires_key]
|
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
-
ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode)
|
|
209
|
+
ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode, :plan_mode)
|
|
210
210
|
|
|
211
211
|
def run_chat_loop(context)
|
|
212
212
|
loop do
|
|
@@ -215,7 +215,7 @@ def run_chat_loop(context)
|
|
|
215
215
|
|
|
216
216
|
next if handle_special_command(user_input, context)
|
|
217
217
|
|
|
218
|
-
process_user_message(context.client, user_input)
|
|
218
|
+
process_user_message(context.client, user_input, context)
|
|
219
219
|
end
|
|
220
220
|
end
|
|
221
221
|
|
|
@@ -237,6 +237,32 @@ def handle_special_command(input, context)
|
|
|
237
237
|
when "config"
|
|
238
238
|
show_config_and_reconfigure(context)
|
|
239
239
|
true
|
|
240
|
+
when "plan", "plan mode"
|
|
241
|
+
puts RubyCode::Views::Cli::PlanModeEnter.build
|
|
242
|
+
context.plan_mode = true
|
|
243
|
+
true
|
|
244
|
+
when "auto-approve on", "auto-approve write"
|
|
245
|
+
confirmed = context.prompt.yes?(
|
|
246
|
+
"⚠ Enable auto-approve for write/update operations?\n" \
|
|
247
|
+
"Files will be modified without confirmation.",
|
|
248
|
+
default: false
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if confirmed
|
|
252
|
+
context.client.approval_handler.enable_auto_approve_write
|
|
253
|
+
puts RubyCode::Views::Cli::AutoApproveEnabled.build
|
|
254
|
+
else
|
|
255
|
+
puts "\nAuto-approve NOT enabled.\n\n"
|
|
256
|
+
end
|
|
257
|
+
true
|
|
258
|
+
when "auto-approve off"
|
|
259
|
+
context.client.approval_handler.disable_auto_approve_write
|
|
260
|
+
puts RubyCode::Views::Cli::AutoApproveDisabled.build
|
|
261
|
+
true
|
|
262
|
+
when "auto-approve", "auto-approve status"
|
|
263
|
+
status = context.client.approval_handler.auto_approve_write_enabled
|
|
264
|
+
puts RubyCode::Views::Cli::AutoApproveStatus.build(enabled: status)
|
|
265
|
+
true
|
|
240
266
|
else
|
|
241
267
|
false
|
|
242
268
|
end
|
|
@@ -247,7 +273,8 @@ def show_config_and_reconfigure(context)
|
|
|
247
273
|
adapter: context.adapter,
|
|
248
274
|
model: context.model,
|
|
249
275
|
directory: context.full_path,
|
|
250
|
-
debug_mode: context.debug_mode
|
|
276
|
+
debug_mode: context.debug_mode,
|
|
277
|
+
auto_approve: context.client.approval_handler.auto_approve_write_enabled
|
|
251
278
|
)
|
|
252
279
|
|
|
253
280
|
return unless context.prompt.yes?(I18n.t("rubycode.setup.reconfigure"), default: false)
|
|
@@ -256,9 +283,40 @@ def show_config_and_reconfigure(context)
|
|
|
256
283
|
setup_wizard(context.prompt)
|
|
257
284
|
end
|
|
258
285
|
|
|
259
|
-
def process_user_message(client, user_input)
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
def process_user_message(client, user_input, context)
|
|
287
|
+
if context.plan_mode
|
|
288
|
+
# In plan mode - use explore tool then ask for approval
|
|
289
|
+
response = client.ask(prompt: "Use the explore tool with this query: #{user_input}")
|
|
290
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
291
|
+
|
|
292
|
+
# Ask user if they accept the plan
|
|
293
|
+
accept_plan = context.prompt.yes?("\nDo you accept this plan and want to proceed with implementation?",
|
|
294
|
+
default: true)
|
|
295
|
+
|
|
296
|
+
if accept_plan
|
|
297
|
+
puts "\n✓ Plan accepted. Auto-approve enabled for implementation.\n"
|
|
298
|
+
# Enable auto-approve for implementation
|
|
299
|
+
client.approval_handler.enable_auto_approve_write
|
|
300
|
+
|
|
301
|
+
# Ask for implementation prompt
|
|
302
|
+
impl_prompt = context.prompt.ask("Describe what you want to implement:")
|
|
303
|
+
response = client.ask(prompt: impl_prompt)
|
|
304
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
305
|
+
|
|
306
|
+
# Disable auto-approve after implementation
|
|
307
|
+
client.approval_handler.disable_auto_approve_write
|
|
308
|
+
else
|
|
309
|
+
puts "\n✗ Plan rejected. Returning to normal mode.\n"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Exit plan mode after exploration
|
|
313
|
+
context.plan_mode = false
|
|
314
|
+
puts RubyCode::Views::Cli::PlanModeExit.build
|
|
315
|
+
else
|
|
316
|
+
# Normal mode
|
|
317
|
+
response = client.ask(prompt: user_input)
|
|
318
|
+
puts RubyCode::Views::Cli::ResponseBox.build(response: response)
|
|
319
|
+
end
|
|
262
320
|
rescue Interrupt
|
|
263
321
|
puts RubyCode::Views::Cli::InterruptMessage.build
|
|
264
322
|
rescue StandardError => e
|
|
@@ -270,5 +328,6 @@ client = RubyCode::Client.new(tty_prompt: prompt)
|
|
|
270
328
|
puts RubyCode::Views::Cli::ReadyMessage.build
|
|
271
329
|
|
|
272
330
|
debug_mode = false # Debug mode not yet implemented
|
|
273
|
-
|
|
331
|
+
plan_mode = false # Plan mode flag
|
|
332
|
+
context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode, plan_mode)
|
|
274
333
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Medeiros
|
|
@@ -232,14 +232,17 @@ files:
|
|
|
232
232
|
- ".env.example"
|
|
233
233
|
- ".rubocop.yml"
|
|
234
234
|
- CHANGELOG.md
|
|
235
|
+
- EXPLORE_TOOL_DESIGN.md
|
|
235
236
|
- LICENSE.txt
|
|
236
237
|
- README.md
|
|
237
238
|
- Rakefile
|
|
238
239
|
- USAGE.md
|
|
240
|
+
- config/exploration_prompt.md
|
|
239
241
|
- config/locales/en.yml
|
|
240
242
|
- config/system_prompt.md
|
|
241
243
|
- config/tools/bash.json
|
|
242
244
|
- config/tools/done.json
|
|
245
|
+
- config/tools/explore.json
|
|
243
246
|
- config/tools/fetch.json
|
|
244
247
|
- config/tools/read.json
|
|
245
248
|
- config/tools/search.json
|
|
@@ -268,6 +271,7 @@ files:
|
|
|
268
271
|
- lib/rubycode/context_builder.rb
|
|
269
272
|
- lib/rubycode/database.rb
|
|
270
273
|
- lib/rubycode/errors.rb
|
|
274
|
+
- lib/rubycode/explorer.rb
|
|
271
275
|
- lib/rubycode/models.rb
|
|
272
276
|
- lib/rubycode/models/api_key.rb
|
|
273
277
|
- lib/rubycode/models/base.rb
|
|
@@ -286,6 +290,7 @@ files:
|
|
|
286
290
|
- lib/rubycode/tools/base.rb
|
|
287
291
|
- lib/rubycode/tools/bash.rb
|
|
288
292
|
- lib/rubycode/tools/done.rb
|
|
293
|
+
- lib/rubycode/tools/explore.rb
|
|
289
294
|
- lib/rubycode/tools/fetch.rb
|
|
290
295
|
- lib/rubycode/tools/read.rb
|
|
291
296
|
- lib/rubycode/tools/search.rb
|
|
@@ -295,6 +300,7 @@ files:
|
|
|
295
300
|
- lib/rubycode/value_objects.rb
|
|
296
301
|
- lib/rubycode/version.rb
|
|
297
302
|
- lib/rubycode/views.rb
|
|
303
|
+
- lib/rubycode/views/adapter.rb
|
|
298
304
|
- lib/rubycode/views/adapter/debug_delay.rb
|
|
299
305
|
- lib/rubycode/views/adapter/debug_request.rb
|
|
300
306
|
- lib/rubycode/views/adapter/debug_response.rb
|
|
@@ -310,6 +316,9 @@ files:
|
|
|
310
316
|
- lib/rubycode/views/bash_approval.rb
|
|
311
317
|
- lib/rubycode/views/cli.rb
|
|
312
318
|
- lib/rubycode/views/cli/api_key_missing.rb
|
|
319
|
+
- lib/rubycode/views/cli/auto_approve_disabled.rb
|
|
320
|
+
- lib/rubycode/views/cli/auto_approve_enabled.rb
|
|
321
|
+
- lib/rubycode/views/cli/auto_approve_status.rb
|
|
313
322
|
- lib/rubycode/views/cli/config_saved.rb
|
|
314
323
|
- lib/rubycode/views/cli/configuration_table.rb
|
|
315
324
|
- lib/rubycode/views/cli/error_display.rb
|
|
@@ -318,6 +327,8 @@ files:
|
|
|
318
327
|
- lib/rubycode/views/cli/first_time_setup.rb
|
|
319
328
|
- lib/rubycode/views/cli/interrupt_message.rb
|
|
320
329
|
- lib/rubycode/views/cli/memory_cleared_message.rb
|
|
330
|
+
- lib/rubycode/views/cli/plan_mode_enter.rb
|
|
331
|
+
- lib/rubycode/views/cli/plan_mode_exit.rb
|
|
321
332
|
- lib/rubycode/views/cli/ready_message.rb
|
|
322
333
|
- lib/rubycode/views/cli/response_box.rb
|
|
323
334
|
- lib/rubycode/views/cli/restart_message.rb
|