rails_console_ai 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +95 -0
- data/LICENSE +21 -0
- data/README.md +328 -0
- data/app/controllers/rails_console_ai/application_controller.rb +28 -0
- data/app/controllers/rails_console_ai/sessions_controller.rb +16 -0
- data/app/helpers/rails_console_ai/sessions_helper.rb +56 -0
- data/app/models/rails_console_ai/session.rb +23 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +84 -0
- data/app/views/rails_console_ai/sessions/index.html.erb +57 -0
- data/app/views/rails_console_ai/sessions/show.html.erb +66 -0
- data/config/routes.rb +4 -0
- data/lib/generators/rails_console_ai/install_generator.rb +26 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +79 -0
- data/lib/rails_console_ai/channel/base.rb +23 -0
- data/lib/rails_console_ai/channel/console.rb +457 -0
- data/lib/rails_console_ai/channel/slack.rb +182 -0
- data/lib/rails_console_ai/configuration.rb +185 -0
- data/lib/rails_console_ai/console_methods.rb +277 -0
- data/lib/rails_console_ai/context_builder.rb +120 -0
- data/lib/rails_console_ai/conversation_engine.rb +1142 -0
- data/lib/rails_console_ai/engine.rb +5 -0
- data/lib/rails_console_ai/executor.rb +461 -0
- data/lib/rails_console_ai/providers/anthropic.rb +122 -0
- data/lib/rails_console_ai/providers/base.rb +118 -0
- data/lib/rails_console_ai/providers/bedrock.rb +171 -0
- data/lib/rails_console_ai/providers/local.rb +112 -0
- data/lib/rails_console_ai/providers/openai.rb +114 -0
- data/lib/rails_console_ai/railtie.rb +34 -0
- data/lib/rails_console_ai/repl.rb +65 -0
- data/lib/rails_console_ai/safety_guards.rb +207 -0
- data/lib/rails_console_ai/session_logger.rb +90 -0
- data/lib/rails_console_ai/slack_bot.rb +473 -0
- data/lib/rails_console_ai/storage/base.rb +27 -0
- data/lib/rails_console_ai/storage/file_storage.rb +63 -0
- data/lib/rails_console_ai/tools/code_tools.rb +126 -0
- data/lib/rails_console_ai/tools/memory_tools.rb +136 -0
- data/lib/rails_console_ai/tools/model_tools.rb +95 -0
- data/lib/rails_console_ai/tools/registry.rb +478 -0
- data/lib/rails_console_ai/tools/schema_tools.rb +60 -0
- data/lib/rails_console_ai/version.rb +3 -0
- data/lib/rails_console_ai.rb +214 -0
- data/lib/tasks/rails_console_ai.rake +7 -0
- metadata +152 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module RailsConsoleAI
|
|
4
|
+
module Tools
|
|
5
|
+
class Registry
|
|
6
|
+
attr_reader :definitions
|
|
7
|
+
|
|
8
|
+
# Tools that should never be cached (side effects or user interaction)
|
|
9
|
+
NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(executor: nil, mode: :default, channel: nil)
|
|
12
|
+
@executor = executor
|
|
13
|
+
@mode = mode
|
|
14
|
+
@channel = channel
|
|
15
|
+
@definitions = []
|
|
16
|
+
@handlers = {}
|
|
17
|
+
@cache = {}
|
|
18
|
+
@last_cached = false
|
|
19
|
+
register_all
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def last_cached?
|
|
23
|
+
@last_cached
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute(tool_name, arguments = {})
|
|
27
|
+
handler = @handlers[tool_name]
|
|
28
|
+
unless handler
|
|
29
|
+
return "Error: unknown tool '#{tool_name}'"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
args = if arguments.is_a?(String)
|
|
33
|
+
begin
|
|
34
|
+
JSON.parse(arguments)
|
|
35
|
+
rescue
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
arguments || {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
unless NO_CACHE.include?(tool_name)
|
|
43
|
+
cache_key = [tool_name, args].hash
|
|
44
|
+
if @cache.key?(cache_key)
|
|
45
|
+
@last_cached = true
|
|
46
|
+
return @cache[cache_key]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@last_cached = false
|
|
51
|
+
result = handler.call(args)
|
|
52
|
+
@cache[[tool_name, args].hash] = result unless NO_CACHE.include?(tool_name)
|
|
53
|
+
result
|
|
54
|
+
rescue => e
|
|
55
|
+
"Error executing #{tool_name}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_anthropic_format
|
|
59
|
+
definitions.map do |d|
|
|
60
|
+
tool = {
|
|
61
|
+
'name' => d[:name],
|
|
62
|
+
'description' => d[:description],
|
|
63
|
+
'input_schema' => d[:parameters]
|
|
64
|
+
}
|
|
65
|
+
tool
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_bedrock_format
|
|
70
|
+
definitions.map do |d|
|
|
71
|
+
{
|
|
72
|
+
tool_spec: {
|
|
73
|
+
name: d[:name],
|
|
74
|
+
description: d[:description],
|
|
75
|
+
input_schema: { json: d[:parameters] }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_openai_format
|
|
82
|
+
definitions.map do |d|
|
|
83
|
+
{
|
|
84
|
+
'type' => 'function',
|
|
85
|
+
'function' => {
|
|
86
|
+
'name' => d[:name],
|
|
87
|
+
'description' => d[:description],
|
|
88
|
+
'parameters' => d[:parameters]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def register_all
|
|
97
|
+
require 'rails_console_ai/tools/schema_tools'
|
|
98
|
+
require 'rails_console_ai/tools/model_tools'
|
|
99
|
+
require 'rails_console_ai/tools/code_tools'
|
|
100
|
+
|
|
101
|
+
schema = SchemaTools.new
|
|
102
|
+
models = ModelTools.new
|
|
103
|
+
code = CodeTools.new
|
|
104
|
+
|
|
105
|
+
register(
|
|
106
|
+
name: 'list_tables',
|
|
107
|
+
description: 'List all database table names in this Rails app.',
|
|
108
|
+
parameters: { 'type' => 'object', 'properties' => {} },
|
|
109
|
+
handler: ->(_args) { schema.list_tables }
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
register(
|
|
113
|
+
name: 'describe_table',
|
|
114
|
+
description: 'Get column names and types for a specific database table.',
|
|
115
|
+
parameters: {
|
|
116
|
+
'type' => 'object',
|
|
117
|
+
'properties' => {
|
|
118
|
+
'table_name' => { 'type' => 'string', 'description' => 'The database table name (e.g. "users")' }
|
|
119
|
+
},
|
|
120
|
+
'required' => ['table_name']
|
|
121
|
+
},
|
|
122
|
+
handler: ->(args) { schema.describe_table(args['table_name']) }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
register(
|
|
126
|
+
name: 'list_models',
|
|
127
|
+
description: 'List all ActiveRecord model names with their association names.',
|
|
128
|
+
parameters: { 'type' => 'object', 'properties' => {} },
|
|
129
|
+
handler: ->(_args) { models.list_models }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
register(
|
|
133
|
+
name: 'describe_model',
|
|
134
|
+
description: 'Get detailed info about a specific model: associations, validations, table name.',
|
|
135
|
+
parameters: {
|
|
136
|
+
'type' => 'object',
|
|
137
|
+
'properties' => {
|
|
138
|
+
'model_name' => { 'type' => 'string', 'description' => 'The model class name (e.g. "User")' }
|
|
139
|
+
},
|
|
140
|
+
'required' => ['model_name']
|
|
141
|
+
},
|
|
142
|
+
handler: ->(args) { models.describe_model(args['model_name']) }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
register(
|
|
146
|
+
name: 'list_files',
|
|
147
|
+
description: 'List Ruby files in a directory of this Rails app. Defaults to app/ directory.',
|
|
148
|
+
parameters: {
|
|
149
|
+
'type' => 'object',
|
|
150
|
+
'properties' => {
|
|
151
|
+
'directory' => { 'type' => 'string', 'description' => 'Relative directory path (e.g. "app/models", "lib"). Defaults to "app".' }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
handler: ->(args) { code.list_files(args['directory']) }
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
register(
|
|
158
|
+
name: 'read_file',
|
|
159
|
+
description: 'Read the contents of a file in this Rails app. Returns up to 500 lines by default. Use start_line/end_line to read specific sections of large files.',
|
|
160
|
+
parameters: {
|
|
161
|
+
'type' => 'object',
|
|
162
|
+
'properties' => {
|
|
163
|
+
'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' },
|
|
164
|
+
'start_line' => { 'type' => 'integer', 'description' => 'First line to read (1-based). Optional — omit to start from beginning.' },
|
|
165
|
+
'end_line' => { 'type' => 'integer', 'description' => 'Last line to read (1-based, inclusive). Optional — omit to read to end.' }
|
|
166
|
+
},
|
|
167
|
+
'required' => ['path']
|
|
168
|
+
},
|
|
169
|
+
handler: ->(args) { code.read_file(args['path'], start_line: args['start_line'], end_line: args['end_line']) }
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
register(
|
|
173
|
+
name: 'search_code',
|
|
174
|
+
description: 'Search for a pattern in Ruby files. Returns matching lines with file paths.',
|
|
175
|
+
parameters: {
|
|
176
|
+
'type' => 'object',
|
|
177
|
+
'properties' => {
|
|
178
|
+
'query' => { 'type' => 'string', 'description' => 'Search pattern (substring match)' },
|
|
179
|
+
'directory' => { 'type' => 'string', 'description' => 'Relative directory to search in. Defaults to "app".' }
|
|
180
|
+
},
|
|
181
|
+
'required' => ['query']
|
|
182
|
+
},
|
|
183
|
+
handler: ->(args) { code.search_code(args['query'], args['directory']) }
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if @executor
|
|
187
|
+
register(
|
|
188
|
+
name: 'recall_output',
|
|
189
|
+
description: 'Retrieve a previous code execution output that was omitted from the conversation to save context. Use the output id shown in the "[Output omitted]" placeholder.',
|
|
190
|
+
parameters: {
|
|
191
|
+
'type' => 'object',
|
|
192
|
+
'properties' => {
|
|
193
|
+
'id' => { 'type' => 'integer', 'description' => 'The output id to retrieve' }
|
|
194
|
+
},
|
|
195
|
+
'required' => ['id']
|
|
196
|
+
},
|
|
197
|
+
handler: ->(args) {
|
|
198
|
+
result = @executor.recall_output(args['id'].to_i)
|
|
199
|
+
result || "No output found with id #{args['id']}"
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless @mode == :init
|
|
205
|
+
register(
|
|
206
|
+
name: 'ask_user',
|
|
207
|
+
description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
|
|
208
|
+
parameters: {
|
|
209
|
+
'type' => 'object',
|
|
210
|
+
'properties' => {
|
|
211
|
+
'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
|
|
212
|
+
},
|
|
213
|
+
'required' => ['question']
|
|
214
|
+
},
|
|
215
|
+
handler: ->(args) { ask_user(args['question']) }
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
register_memory_tools
|
|
219
|
+
register_execute_plan
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def register_memory_tools
|
|
224
|
+
return unless RailsConsoleAI.configuration.memories_enabled
|
|
225
|
+
|
|
226
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
227
|
+
memory = MemoryTools.new
|
|
228
|
+
|
|
229
|
+
register(
|
|
230
|
+
name: 'save_memory',
|
|
231
|
+
description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic).',
|
|
232
|
+
parameters: {
|
|
233
|
+
'type' => 'object',
|
|
234
|
+
'properties' => {
|
|
235
|
+
'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
|
|
236
|
+
'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
|
|
237
|
+
'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
|
|
238
|
+
},
|
|
239
|
+
'required' => ['name', 'description']
|
|
240
|
+
},
|
|
241
|
+
handler: ->(args) {
|
|
242
|
+
memory.save_memory(name: args['name'], description: args['description'], tags: args['tags'] || [])
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
register(
|
|
247
|
+
name: 'delete_memory',
|
|
248
|
+
description: 'Delete a memory by name.',
|
|
249
|
+
parameters: {
|
|
250
|
+
'type' => 'object',
|
|
251
|
+
'properties' => {
|
|
252
|
+
'name' => { 'type' => 'string', 'description' => 'The memory name to delete (e.g. "Sharding architecture")' }
|
|
253
|
+
},
|
|
254
|
+
'required' => ['name']
|
|
255
|
+
},
|
|
256
|
+
handler: ->(args) { memory.delete_memory(name: args['name']) }
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
register(
|
|
260
|
+
name: 'recall_memories',
|
|
261
|
+
description: 'Search your saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
|
|
262
|
+
parameters: {
|
|
263
|
+
'type' => 'object',
|
|
264
|
+
'properties' => {
|
|
265
|
+
'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
|
|
266
|
+
'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def register_execute_plan
|
|
274
|
+
return unless @executor
|
|
275
|
+
|
|
276
|
+
register(
|
|
277
|
+
name: 'execute_plan',
|
|
278
|
+
description: 'Execute a multi-step plan. Each step has a description and Ruby code. The plan is shown to the user for approval, then each step is executed in order. After each step executes, its return value is stored as step1, step2, etc. Use these variables in later steps to reference earlier results (e.g. `token = step1`).',
|
|
279
|
+
parameters: {
|
|
280
|
+
'type' => 'object',
|
|
281
|
+
'properties' => {
|
|
282
|
+
'steps' => {
|
|
283
|
+
'type' => 'array',
|
|
284
|
+
'description' => 'Ordered list of steps to execute',
|
|
285
|
+
'items' => {
|
|
286
|
+
'type' => 'object',
|
|
287
|
+
'properties' => {
|
|
288
|
+
'description' => { 'type' => 'string', 'description' => 'What this step does' },
|
|
289
|
+
'code' => { 'type' => 'string', 'description' => 'Ruby code to execute' }
|
|
290
|
+
},
|
|
291
|
+
'required' => %w[description code]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
'required' => ['steps']
|
|
296
|
+
},
|
|
297
|
+
handler: ->(args) { execute_plan(args['steps'] || []) }
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def execute_plan(steps)
|
|
302
|
+
return 'No steps provided.' if steps.nil? || steps.empty?
|
|
303
|
+
|
|
304
|
+
auto = RailsConsoleAI.configuration.auto_execute
|
|
305
|
+
|
|
306
|
+
# Display full plan
|
|
307
|
+
$stdout.puts
|
|
308
|
+
$stdout.puts "\e[36m Plan (#{steps.length} steps):\e[0m"
|
|
309
|
+
steps.each_with_index do |step, i|
|
|
310
|
+
$stdout.puts "\e[36m #{i + 1}. #{step['description']}\e[0m"
|
|
311
|
+
$stdout.puts highlight_plan_code(step['code'])
|
|
312
|
+
end
|
|
313
|
+
$stdout.puts
|
|
314
|
+
|
|
315
|
+
# Ask for plan approval (unless auto-execute)
|
|
316
|
+
skip_confirmations = auto
|
|
317
|
+
unless auto
|
|
318
|
+
if @channel
|
|
319
|
+
answer = @channel.confirm(" Accept plan? [y/N/a(uto)] ")
|
|
320
|
+
else
|
|
321
|
+
$stdout.print "\e[33m Accept plan? [y/N/a(uto)] \e[0m"
|
|
322
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
323
|
+
end
|
|
324
|
+
case answer
|
|
325
|
+
when 'a', 'auto'
|
|
326
|
+
skip_confirmations = true
|
|
327
|
+
when 'y', 'yes'
|
|
328
|
+
skip_confirmations = true if steps.length == 1
|
|
329
|
+
else
|
|
330
|
+
$stdout.puts "\e[33m Plan declined.\e[0m"
|
|
331
|
+
feedback = ask_feedback("What would you like changed?")
|
|
332
|
+
return "User declined the plan. Feedback: #{feedback}"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Execute steps one by one
|
|
337
|
+
results = []
|
|
338
|
+
steps.each_with_index do |step, i|
|
|
339
|
+
$stdout.puts
|
|
340
|
+
$stdout.puts "\e[36m Step #{i + 1}/#{steps.length}: #{step['description']}\e[0m"
|
|
341
|
+
$stdout.puts "\e[33m # Code:\e[0m"
|
|
342
|
+
$stdout.puts highlight_plan_code(step['code'])
|
|
343
|
+
|
|
344
|
+
# Per-step confirmation (unless auto-execute or plan-level auto)
|
|
345
|
+
unless skip_confirmations
|
|
346
|
+
if @channel
|
|
347
|
+
step_answer = @channel.confirm(" Run? [y/N/edit] ")
|
|
348
|
+
else
|
|
349
|
+
$stdout.print "\e[33m Run? [y/N/edit] \e[0m"
|
|
350
|
+
step_answer = $stdin.gets.to_s.strip.downcase
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
case step_answer
|
|
354
|
+
when 'e', 'edit'
|
|
355
|
+
edited = edit_step_code(step['code'])
|
|
356
|
+
if edited && edited != step['code']
|
|
357
|
+
$stdout.puts "\e[33m # Edited code:\e[0m"
|
|
358
|
+
$stdout.puts highlight_plan_code(edited)
|
|
359
|
+
if @channel
|
|
360
|
+
confirm = @channel.confirm(" Run edited code? [y/N] ")
|
|
361
|
+
else
|
|
362
|
+
$stdout.print "\e[33m Run edited code? [y/N] \e[0m"
|
|
363
|
+
confirm = $stdin.gets.to_s.strip.downcase
|
|
364
|
+
end
|
|
365
|
+
unless confirm == 'y' || confirm == 'yes'
|
|
366
|
+
feedback = ask_feedback("What would you like changed?")
|
|
367
|
+
results << "Step #{i + 1}: User declined after edit. Feedback: #{feedback}"
|
|
368
|
+
break
|
|
369
|
+
end
|
|
370
|
+
step['code'] = edited
|
|
371
|
+
end
|
|
372
|
+
when 'y', 'yes'
|
|
373
|
+
# proceed
|
|
374
|
+
else
|
|
375
|
+
feedback = ask_feedback("What would you like changed?")
|
|
376
|
+
results << "Step #{i + 1}: User declined. Feedback: #{feedback}"
|
|
377
|
+
break
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
exec_result = @executor.execute(step['code'])
|
|
382
|
+
|
|
383
|
+
# On safety error, offer to re-run with guards disabled (console only)
|
|
384
|
+
if @executor.last_safety_error
|
|
385
|
+
if @channel && !@channel.supports_danger?
|
|
386
|
+
results << "Step #{i + 1} (#{step['description']}):\nBLOCKED by safety guard: #{@executor.last_error}. Write operations are not permitted in this channel."
|
|
387
|
+
break
|
|
388
|
+
else
|
|
389
|
+
exec_result = @executor.offer_danger_retry(step['code'])
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Make result available as step1, step2, etc. for subsequent steps
|
|
394
|
+
@executor.binding_context.local_variable_set(:"step#{i + 1}", exec_result)
|
|
395
|
+
output = @executor.last_output
|
|
396
|
+
error = @executor.last_error
|
|
397
|
+
|
|
398
|
+
step_report = "Step #{i + 1} (#{step['description']}):\n"
|
|
399
|
+
if error
|
|
400
|
+
step_report += "ERROR: #{error}\n"
|
|
401
|
+
end
|
|
402
|
+
if output && !output.strip.empty?
|
|
403
|
+
step_report += "Output: #{output.strip}\n"
|
|
404
|
+
end
|
|
405
|
+
step_report += "Return value: #{exec_result.inspect}"
|
|
406
|
+
results << step_report
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
results.join("\n\n")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def highlight_plan_code(code)
|
|
413
|
+
if coderay_available?
|
|
414
|
+
CodeRay.scan(code, :ruby).terminal.gsub(/^/, ' ')
|
|
415
|
+
else
|
|
416
|
+
code.split("\n").map { |l| " \e[37m#{l}\e[0m" }.join("\n")
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def edit_step_code(code)
|
|
421
|
+
require 'tempfile'
|
|
422
|
+
editor = ENV['EDITOR'] || 'vi'
|
|
423
|
+
tmpfile = Tempfile.new(['rails_console_ai_step', '.rb'])
|
|
424
|
+
tmpfile.write(code)
|
|
425
|
+
tmpfile.flush
|
|
426
|
+
system("#{editor} #{tmpfile.path}")
|
|
427
|
+
File.read(tmpfile.path).strip
|
|
428
|
+
rescue => e
|
|
429
|
+
$stderr.puts "\e[31m Editor error: #{e.message}\e[0m"
|
|
430
|
+
code
|
|
431
|
+
ensure
|
|
432
|
+
tmpfile.close! if tmpfile
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def coderay_available?
|
|
436
|
+
return @coderay_available unless @coderay_available.nil?
|
|
437
|
+
@coderay_available = begin
|
|
438
|
+
require 'coderay'
|
|
439
|
+
true
|
|
440
|
+
rescue LoadError
|
|
441
|
+
false
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def ask_feedback(prompt)
|
|
446
|
+
if @channel
|
|
447
|
+
@channel.prompt(" #{prompt} > ")
|
|
448
|
+
else
|
|
449
|
+
$stdout.print "\e[36m #{prompt} > \e[0m"
|
|
450
|
+
feedback = $stdin.gets
|
|
451
|
+
return '(no feedback provided)' if feedback.nil?
|
|
452
|
+
feedback.strip.empty? ? '(no feedback provided)' : feedback.strip
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def ask_user(question)
|
|
457
|
+
if @channel
|
|
458
|
+
@channel.prompt(" ? #{question}\n > ")
|
|
459
|
+
else
|
|
460
|
+
$stdout.puts "\e[36m ? #{question}\e[0m"
|
|
461
|
+
$stdout.print "\e[36m > \e[0m"
|
|
462
|
+
answer = $stdin.gets
|
|
463
|
+
return '(no answer provided)' if answer.nil?
|
|
464
|
+
answer.strip.empty? ? '(no answer provided)' : answer.strip
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def register(name:, description:, parameters:, handler:)
|
|
469
|
+
@definitions << {
|
|
470
|
+
name: name,
|
|
471
|
+
description: description,
|
|
472
|
+
parameters: parameters
|
|
473
|
+
}
|
|
474
|
+
@handlers[name] = handler
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module RailsConsoleAI
|
|
2
|
+
module Tools
|
|
3
|
+
class SchemaTools
|
|
4
|
+
def list_tables
|
|
5
|
+
return "ActiveRecord is not connected." unless ar_connected?
|
|
6
|
+
|
|
7
|
+
tables = connection.tables.sort
|
|
8
|
+
tables.reject! { |t| t == 'schema_migrations' || t == 'ar_internal_metadata' }
|
|
9
|
+
return "No tables found." if tables.empty?
|
|
10
|
+
|
|
11
|
+
tables.join(", ")
|
|
12
|
+
rescue => e
|
|
13
|
+
"Error listing tables: #{e.message}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def describe_table(table_name)
|
|
17
|
+
return "ActiveRecord is not connected." unless ar_connected?
|
|
18
|
+
return "Error: table_name is required." if table_name.nil? || table_name.strip.empty?
|
|
19
|
+
|
|
20
|
+
table_name = table_name.strip
|
|
21
|
+
unless connection.tables.include?(table_name)
|
|
22
|
+
return "Table '#{table_name}' not found. Use list_tables to see available tables."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
cols = connection.columns(table_name).map do |c|
|
|
26
|
+
parts = ["#{c.name}:#{c.type}"]
|
|
27
|
+
parts << "nullable" if c.null
|
|
28
|
+
parts << "default=#{c.default}" unless c.default.nil?
|
|
29
|
+
parts.join(" ")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
indexes = connection.indexes(table_name).map do |idx|
|
|
33
|
+
unique = idx.unique ? "UNIQUE " : ""
|
|
34
|
+
"#{unique}INDEX on (#{idx.columns.join(', ')})"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result = "Table: #{table_name}\n"
|
|
38
|
+
result += "Columns:\n"
|
|
39
|
+
cols.each { |c| result += " #{c}\n" }
|
|
40
|
+
unless indexes.empty?
|
|
41
|
+
result += "Indexes:\n"
|
|
42
|
+
indexes.each { |i| result += " #{i}\n" }
|
|
43
|
+
end
|
|
44
|
+
result
|
|
45
|
+
rescue => e
|
|
46
|
+
"Error describing table '#{table_name}': #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def ar_connected?
|
|
52
|
+
defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def connection
|
|
56
|
+
ActiveRecord::Base.connection
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|