console_agent 0.2.0 → 0.3.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 +4 -4
- data/README.md +117 -231
- data/app/controllers/console_agent/application_controller.rb +21 -0
- data/app/controllers/console_agent/sessions_controller.rb +16 -0
- data/app/helpers/console_agent/sessions_helper.rb +42 -0
- data/app/models/console_agent/session.rb +23 -0
- data/app/views/console_agent/sessions/index.html.erb +57 -0
- data/app/views/console_agent/sessions/show.html.erb +56 -0
- data/app/views/layouts/console_agent/application.html.erb +83 -0
- data/config/routes.rb +4 -0
- data/lib/console_agent/configuration.rb +8 -4
- data/lib/console_agent/console_methods.rb +125 -5
- data/lib/console_agent/context_builder.rb +12 -132
- data/lib/console_agent/engine.rb +5 -0
- data/lib/console_agent/executor.rb +19 -1
- data/lib/console_agent/repl.rb +299 -34
- data/lib/console_agent/session_logger.rb +79 -0
- data/lib/console_agent/tools/registry.rb +156 -2
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +125 -3
- data/lib/generators/console_agent/templates/initializer.rb +14 -6
- metadata +11 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<a href="<%= console_agent.sessions_path %>" class="back-link">← Back to sessions</a>
|
|
2
|
+
|
|
3
|
+
<div class="meta-card">
|
|
4
|
+
<div class="meta-grid">
|
|
5
|
+
<div class="meta-item">
|
|
6
|
+
<label>Query</label>
|
|
7
|
+
<span><%= @session.query %></span>
|
|
8
|
+
</div>
|
|
9
|
+
<% if @session.name.present? %>
|
|
10
|
+
<div class="meta-item">
|
|
11
|
+
<label>Name</label>
|
|
12
|
+
<span><%= @session.name %></span>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<div class="meta-item">
|
|
16
|
+
<label>Mode</label>
|
|
17
|
+
<span class="badge badge-<%= @session.mode %>"><%= @session.mode %></span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="meta-item">
|
|
20
|
+
<label>User</label>
|
|
21
|
+
<span><%= @session.user_name || '-' %></span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="meta-item">
|
|
24
|
+
<label>Provider / Model</label>
|
|
25
|
+
<span><%= @session.provider %> / <%= @session.model %></span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="meta-item">
|
|
28
|
+
<label>Tokens (in / out)</label>
|
|
29
|
+
<span class="mono"><%= @session.input_tokens %> / <%= @session.output_tokens %></span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="meta-item">
|
|
32
|
+
<label>Duration</label>
|
|
33
|
+
<span class="mono"><%= @session.duration_ms ? "#{@session.duration_ms}ms" : '-' %></span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="meta-item">
|
|
36
|
+
<label>Executed?</label>
|
|
37
|
+
<span><%= @session.executed? ? '<span class="check">Yes</span>'.html_safe : '<span class="cross">No</span>'.html_safe %></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="meta-item">
|
|
40
|
+
<label>Time</label>
|
|
41
|
+
<span><%= @session.created_at.strftime('%Y-%m-%d %H:%M:%S') %></span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<h3 style="font-size: 16px; margin-bottom: 12px;">Console Output</h3>
|
|
47
|
+
|
|
48
|
+
<% if @session.console_output.present? %>
|
|
49
|
+
<div class="terminal">
|
|
50
|
+
<pre><%= ansi_to_html(@session.console_output) %></pre>
|
|
51
|
+
</div>
|
|
52
|
+
<% else %>
|
|
53
|
+
<div class="meta-card">
|
|
54
|
+
<p class="text-muted">No console output recorded for this session.</p>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>ConsoleAgent Admin</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
10
|
+
background: #f5f5f5;
|
|
11
|
+
color: #333;
|
|
12
|
+
line-height: 1.6;
|
|
13
|
+
}
|
|
14
|
+
.header {
|
|
15
|
+
background: #1a1a2e;
|
|
16
|
+
color: #fff;
|
|
17
|
+
padding: 16px 24px;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
}
|
|
22
|
+
.header h1 { font-size: 18px; font-weight: 600; }
|
|
23
|
+
.header a { color: #8be9fd; text-decoration: none; font-size: 14px; }
|
|
24
|
+
.container { max-width: 1200px; margin: 24px auto; padding: 0 24px; }
|
|
25
|
+
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
26
|
+
th { background: #f8f9fa; text-align: left; padding: 12px 16px; font-size: 13px; font-weight: 600; color: #555; border-bottom: 2px solid #e9ecef; }
|
|
27
|
+
td { padding: 10px 16px; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
|
|
28
|
+
tr:hover td { background: #f8f9fa; }
|
|
29
|
+
a { color: #4a6fa5; text-decoration: none; }
|
|
30
|
+
a:hover { text-decoration: underline; }
|
|
31
|
+
.badge {
|
|
32
|
+
display: inline-block; padding: 2px 8px; border-radius: 12px;
|
|
33
|
+
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
34
|
+
}
|
|
35
|
+
.badge-one_shot { background: #d4edda; color: #155724; }
|
|
36
|
+
.badge-interactive { background: #cce5ff; color: #004085; }
|
|
37
|
+
.badge-explain { background: #fff3cd; color: #856404; }
|
|
38
|
+
.meta-card {
|
|
39
|
+
background: #fff; border-radius: 8px; padding: 20px;
|
|
40
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px;
|
|
41
|
+
}
|
|
42
|
+
.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
|
43
|
+
.meta-item label { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; display: block; }
|
|
44
|
+
.meta-item span { font-size: 14px; color: #333; }
|
|
45
|
+
.terminal {
|
|
46
|
+
background: #1e1e1e; border-radius: 8px; overflow: hidden;
|
|
47
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
48
|
+
}
|
|
49
|
+
.terminal pre {
|
|
50
|
+
background: transparent; color: #d4d4d4;
|
|
51
|
+
padding: 16px 20px; margin: 0;
|
|
52
|
+
font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace;
|
|
53
|
+
font-size: 13px; line-height: 1.5;
|
|
54
|
+
overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
|
|
55
|
+
}
|
|
56
|
+
.mono { font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace; font-size: 13px; }
|
|
57
|
+
.text-muted { color: #888; }
|
|
58
|
+
.back-link { margin-bottom: 16px; display: inline-block; }
|
|
59
|
+
.check { color: #28a745; }
|
|
60
|
+
.cross { color: #dc3545; }
|
|
61
|
+
.pagination {
|
|
62
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
63
|
+
margin-top: 16px; padding: 12px 0;
|
|
64
|
+
}
|
|
65
|
+
.pagination a {
|
|
66
|
+
padding: 6px 14px; background: #fff; border: 1px solid #ddd; border-radius: 4px;
|
|
67
|
+
font-size: 13px; color: #4a6fa5;
|
|
68
|
+
}
|
|
69
|
+
.pagination a:hover { background: #f8f9fa; text-decoration: none; }
|
|
70
|
+
.pagination .disabled { padding: 6px 14px; font-size: 13px; color: #ccc; }
|
|
71
|
+
.pagination .page-info { font-size: 13px; color: #888; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div class="header">
|
|
76
|
+
<h1>ConsoleAgent Admin</h1>
|
|
77
|
+
<a href="<%= console_agent.root_path %>">Sessions</a>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="container">
|
|
80
|
+
<%= yield %>
|
|
81
|
+
</div>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
module ConsoleAgent
|
|
2
2
|
class Configuration
|
|
3
3
|
PROVIDERS = %i[anthropic openai].freeze
|
|
4
|
-
CONTEXT_MODES = %i[full smart].freeze
|
|
5
4
|
|
|
6
5
|
attr_accessor :provider, :api_key, :model, :max_tokens,
|
|
7
|
-
:auto_execute, :
|
|
6
|
+
:auto_execute, :temperature,
|
|
8
7
|
:timeout, :debug, :max_tool_rounds,
|
|
9
|
-
:storage_adapter, :memories_enabled
|
|
8
|
+
:storage_adapter, :memories_enabled,
|
|
9
|
+
:session_logging, :connection_class,
|
|
10
|
+
:admin_username, :admin_password
|
|
10
11
|
|
|
11
12
|
def initialize
|
|
12
13
|
@provider = :anthropic
|
|
@@ -14,13 +15,16 @@ module ConsoleAgent
|
|
|
14
15
|
@model = nil
|
|
15
16
|
@max_tokens = 4096
|
|
16
17
|
@auto_execute = false
|
|
17
|
-
@context_mode = :smart
|
|
18
18
|
@temperature = 0.2
|
|
19
19
|
@timeout = 30
|
|
20
20
|
@debug = false
|
|
21
21
|
@max_tool_rounds = 100
|
|
22
22
|
@storage_adapter = nil
|
|
23
23
|
@memories_enabled = true
|
|
24
|
+
@session_logging = true
|
|
25
|
+
@connection_class = nil
|
|
26
|
+
@admin_username = nil
|
|
27
|
+
@admin_password = nil
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def resolved_api_key
|
|
@@ -46,17 +46,99 @@ module ConsoleAgent
|
|
|
46
46
|
nil
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def ai_sessions(n = 10, search: nil)
|
|
50
|
+
require 'console_agent/session_logger'
|
|
51
|
+
session_class = Object.const_get('ConsoleAgent::Session')
|
|
52
|
+
|
|
53
|
+
scope = session_class.recent
|
|
54
|
+
scope = scope.search(search) if search
|
|
55
|
+
sessions = scope.limit(n)
|
|
56
|
+
|
|
57
|
+
if sessions.empty?
|
|
58
|
+
$stdout.puts "\e[2mNo sessions found.\e[0m"
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
$stdout.puts "\e[36m[Sessions — showing #{sessions.length}#{search ? " matching \"#{search}\"" : ''}]\e[0m"
|
|
63
|
+
$stdout.puts
|
|
64
|
+
|
|
65
|
+
sessions.each do |s|
|
|
66
|
+
id_str = "\e[2m##{s.id}\e[0m"
|
|
67
|
+
name_str = s.name ? "\e[33m#{s.name}\e[0m " : ""
|
|
68
|
+
query_str = s.name ? "\e[2m#{truncate_str(s.query, 50)}\e[0m" : truncate_str(s.query, 50)
|
|
69
|
+
mode_str = "\e[2m[#{s.mode}]\e[0m"
|
|
70
|
+
time_str = "\e[2m#{time_ago(s.created_at)}\e[0m"
|
|
71
|
+
tokens = (s.input_tokens || 0) + (s.output_tokens || 0)
|
|
72
|
+
token_str = tokens > 0 ? "\e[2m#{tokens} tokens\e[0m" : ""
|
|
73
|
+
|
|
74
|
+
$stdout.puts " #{id_str} #{name_str}#{query_str}"
|
|
75
|
+
$stdout.puts " #{mode_str} #{time_str} #{token_str}"
|
|
76
|
+
$stdout.puts
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
$stdout.puts "\e[2mUse ai_resume(id_or_name) to resume a session.\e[0m"
|
|
80
|
+
$stdout.puts "\e[2mUse ai_sessions(n, search: \"term\") to filter.\e[0m"
|
|
81
|
+
nil
|
|
82
|
+
rescue => e
|
|
83
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ai_resume(identifier)
|
|
88
|
+
__ensure_console_agent_user
|
|
89
|
+
|
|
90
|
+
require 'console_agent/context_builder'
|
|
91
|
+
require 'console_agent/providers/base'
|
|
92
|
+
require 'console_agent/executor'
|
|
93
|
+
require 'console_agent/repl'
|
|
94
|
+
require 'console_agent/session_logger'
|
|
95
|
+
|
|
96
|
+
session = __find_session(identifier)
|
|
97
|
+
unless session
|
|
98
|
+
$stderr.puts "\e[31mSession not found: #{identifier}\e[0m"
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
repl = Repl.new(__console_agent_binding)
|
|
103
|
+
repl.resume(session)
|
|
104
|
+
rescue => e
|
|
105
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ai_name(identifier, new_name)
|
|
110
|
+
require 'console_agent/session_logger'
|
|
111
|
+
|
|
112
|
+
session = __find_session(identifier)
|
|
113
|
+
unless session
|
|
114
|
+
$stderr.puts "\e[31mSession not found: #{identifier}\e[0m"
|
|
115
|
+
return nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
ConsoleAgent::SessionLogger.update(session.id, name: new_name)
|
|
119
|
+
$stdout.puts "\e[36mSession ##{session.id} named: #{new_name}\e[0m"
|
|
120
|
+
nil
|
|
121
|
+
rescue => e
|
|
122
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
49
126
|
def ai(query = nil)
|
|
50
127
|
if query.nil?
|
|
51
128
|
$stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
|
|
52
|
-
$stderr.puts "\e[33m ai \"query\"
|
|
53
|
-
$stderr.puts "\e[33m ai! \"query\"
|
|
54
|
-
$stderr.puts "\e[33m ai? \"query\"
|
|
55
|
-
$stderr.puts "\e[33m
|
|
56
|
-
$stderr.puts "\e[33m
|
|
129
|
+
$stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
|
|
130
|
+
$stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
|
|
131
|
+
$stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
|
|
132
|
+
$stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
|
|
133
|
+
$stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
|
|
134
|
+
$stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
|
|
135
|
+
$stderr.puts "\e[33m ai_status - show current configuration\e[0m"
|
|
136
|
+
$stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
|
|
57
137
|
return nil
|
|
58
138
|
end
|
|
59
139
|
|
|
140
|
+
__ensure_console_agent_user
|
|
141
|
+
|
|
60
142
|
require 'console_agent/context_builder'
|
|
61
143
|
require 'console_agent/providers/base'
|
|
62
144
|
require 'console_agent/executor'
|
|
@@ -70,6 +152,8 @@ module ConsoleAgent
|
|
|
70
152
|
end
|
|
71
153
|
|
|
72
154
|
def ai!(query = nil)
|
|
155
|
+
__ensure_console_agent_user
|
|
156
|
+
|
|
73
157
|
require 'console_agent/context_builder'
|
|
74
158
|
require 'console_agent/providers/base'
|
|
75
159
|
require 'console_agent/executor'
|
|
@@ -93,6 +177,8 @@ module ConsoleAgent
|
|
|
93
177
|
return nil
|
|
94
178
|
end
|
|
95
179
|
|
|
180
|
+
__ensure_console_agent_user
|
|
181
|
+
|
|
96
182
|
require 'console_agent/context_builder'
|
|
97
183
|
require 'console_agent/providers/base'
|
|
98
184
|
require 'console_agent/executor'
|
|
@@ -107,6 +193,40 @@ module ConsoleAgent
|
|
|
107
193
|
|
|
108
194
|
private
|
|
109
195
|
|
|
196
|
+
def __find_session(identifier)
|
|
197
|
+
session_class = Object.const_get('ConsoleAgent::Session')
|
|
198
|
+
if identifier.is_a?(Integer)
|
|
199
|
+
session_class.find_by(id: identifier)
|
|
200
|
+
else
|
|
201
|
+
session_class.where(name: identifier.to_s).recent.first ||
|
|
202
|
+
session_class.find_by(id: identifier.to_i)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def truncate_str(str, max)
|
|
207
|
+
return '' if str.nil?
|
|
208
|
+
str.length > max ? str[0...max] + '...' : str
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def time_ago(time)
|
|
212
|
+
return '' unless time
|
|
213
|
+
seconds = Time.now - time
|
|
214
|
+
case seconds
|
|
215
|
+
when 0...60 then "just now"
|
|
216
|
+
when 60...3600 then "#{(seconds / 60).to_i}m ago"
|
|
217
|
+
when 3600...86400 then "#{(seconds / 3600).to_i}h ago"
|
|
218
|
+
else "#{(seconds / 86400).to_i}d ago"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def __ensure_console_agent_user
|
|
223
|
+
return if ConsoleAgent.current_user
|
|
224
|
+
$stdout.puts "\e[36mConsoleAgent logs all AI sessions for audit purposes.\e[0m"
|
|
225
|
+
$stdout.print "\e[36mPlease enter your name: \e[0m"
|
|
226
|
+
name = $stdin.gets.to_s.strip
|
|
227
|
+
ConsoleAgent.current_user = name.empty? ? ENV['USER'] : name
|
|
228
|
+
end
|
|
229
|
+
|
|
110
230
|
def __console_agent_binding
|
|
111
231
|
# Try IRB workspace binding
|
|
112
232
|
if defined?(IRB) && IRB.respond_to?(:CurrentContext)
|
|
@@ -5,25 +5,10 @@ module ConsoleAgent
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def build
|
|
8
|
-
|
|
9
|
-
when :smart
|
|
10
|
-
build_smart
|
|
11
|
-
else
|
|
12
|
-
build_full
|
|
13
|
-
end
|
|
8
|
+
build_smart
|
|
14
9
|
rescue => e
|
|
15
10
|
ConsoleAgent.logger.warn("ConsoleAgent: context build error: #{e.message}")
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def build_full
|
|
20
|
-
parts = []
|
|
21
|
-
parts << system_instructions
|
|
22
|
-
parts << environment_context
|
|
23
|
-
parts << schema_context
|
|
24
|
-
parts << models_context
|
|
25
|
-
parts << routes_context
|
|
26
|
-
parts.compact.join("\n\n")
|
|
11
|
+
smart_system_instructions + "\n\n" + environment_context
|
|
27
12
|
end
|
|
28
13
|
|
|
29
14
|
def build_smart
|
|
@@ -60,10 +45,18 @@ module ConsoleAgent
|
|
|
60
45
|
When you use a memory, mention it briefly (e.g. "Based on what I know about sharding...").
|
|
61
46
|
When you discover important patterns about this app, save them as memories.
|
|
62
47
|
|
|
48
|
+
You have an execute_plan tool to run multi-step code. When a task requires multiple
|
|
49
|
+
sequential operations, use execute_plan with an array of steps (each with a description
|
|
50
|
+
and Ruby code). The plan is shown to the user for review before execution begins.
|
|
51
|
+
After each step runs, its return value is stored as step1, step2, etc. — use these
|
|
52
|
+
variables in later steps to reference earlier results (e.g. `api = SalesforceApi.new(step1)`).
|
|
53
|
+
For simple single-expression answers, you may respond with a ```ruby code block instead.
|
|
54
|
+
|
|
63
55
|
RULES:
|
|
64
56
|
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
65
|
-
-
|
|
66
|
-
-
|
|
57
|
+
- For multi-step tasks, use execute_plan to break the work into small, clear steps.
|
|
58
|
+
- For simple queries, respond with a single ```ruby code block.
|
|
59
|
+
- Include a brief one-line explanation before any code block.
|
|
67
60
|
- Use the app's actual model names, associations, and schema.
|
|
68
61
|
- Prefer ActiveRecord query interface over raw SQL.
|
|
69
62
|
- For destructive operations, add a comment warning.
|
|
@@ -74,23 +67,6 @@ module ConsoleAgent
|
|
|
74
67
|
PROMPT
|
|
75
68
|
end
|
|
76
69
|
|
|
77
|
-
def system_instructions
|
|
78
|
-
<<~PROMPT.strip
|
|
79
|
-
You are a Ruby on Rails console assistant. The user is in a `rails console` session.
|
|
80
|
-
You help them query data, debug issues, and understand their application.
|
|
81
|
-
|
|
82
|
-
RULES:
|
|
83
|
-
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
84
|
-
- Respond with a single ```ruby code block that directly answers the question.
|
|
85
|
-
- Include a brief one-line explanation before the code block.
|
|
86
|
-
- Use the app's actual model names, associations, and schema.
|
|
87
|
-
- Prefer ActiveRecord query interface over raw SQL.
|
|
88
|
-
- For destructive operations, add a comment warning.
|
|
89
|
-
- If the request is ambiguous, ask a clarifying question instead of guessing.
|
|
90
|
-
- Keep code concise and idiomatic.
|
|
91
|
-
PROMPT
|
|
92
|
-
end
|
|
93
|
-
|
|
94
70
|
def environment_context
|
|
95
71
|
lines = ["## Environment"]
|
|
96
72
|
lines << "- Ruby #{RUBY_VERSION}"
|
|
@@ -112,93 +88,6 @@ module ConsoleAgent
|
|
|
112
88
|
lines.join("\n")
|
|
113
89
|
end
|
|
114
90
|
|
|
115
|
-
def schema_context
|
|
116
|
-
return nil unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
117
|
-
|
|
118
|
-
conn = ActiveRecord::Base.connection
|
|
119
|
-
tables = conn.tables.sort
|
|
120
|
-
return nil if tables.empty?
|
|
121
|
-
|
|
122
|
-
lines = ["## Database Schema"]
|
|
123
|
-
tables.each do |table|
|
|
124
|
-
next if table == 'schema_migrations' || table == 'ar_internal_metadata'
|
|
125
|
-
|
|
126
|
-
cols = conn.columns(table).map { |c| "#{c.name}:#{c.type}" }
|
|
127
|
-
lines << "- #{table} (#{cols.join(', ')})"
|
|
128
|
-
end
|
|
129
|
-
lines.join("\n")
|
|
130
|
-
rescue => e
|
|
131
|
-
ConsoleAgent.logger.debug("ConsoleAgent: schema introspection failed: #{e.message}")
|
|
132
|
-
nil
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def models_context
|
|
136
|
-
return nil unless defined?(ActiveRecord::Base)
|
|
137
|
-
|
|
138
|
-
eager_load_app!
|
|
139
|
-
base_class = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
|
|
140
|
-
models = ObjectSpace.each_object(Class).select { |c|
|
|
141
|
-
c < base_class && !c.abstract_class? && c.name && !c.name.start_with?('HABTM_')
|
|
142
|
-
}.sort_by(&:name)
|
|
143
|
-
|
|
144
|
-
return nil if models.empty?
|
|
145
|
-
|
|
146
|
-
lines = ["## Models"]
|
|
147
|
-
models.each do |model|
|
|
148
|
-
parts = [model.name]
|
|
149
|
-
|
|
150
|
-
assocs = model.reflect_on_all_associations.map { |a| "#{a.macro} :#{a.name}" }
|
|
151
|
-
parts << " associations: #{assocs.join(', ')}" unless assocs.empty?
|
|
152
|
-
|
|
153
|
-
scopes = model.methods.grep(/^_scope_/).map { |m| m.to_s.sub('_scope_', '') } rescue []
|
|
154
|
-
if scopes.empty?
|
|
155
|
-
# Alternative: check singleton methods that return ActiveRecord::Relation
|
|
156
|
-
scope_names = (model.singleton_methods - base_class.singleton_methods).select { |m|
|
|
157
|
-
m != :all && !m.to_s.start_with?('_') && !m.to_s.start_with?('find')
|
|
158
|
-
}.first(10)
|
|
159
|
-
# We won't list these to avoid noise — scopes are hard to detect reliably
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
validations = model.validators.map { |v|
|
|
163
|
-
attrs = v.attributes.join(', ')
|
|
164
|
-
"#{v.class.name.demodulize.underscore.sub('_validator', '')} on #{attrs}"
|
|
165
|
-
}.uniq.first(5) rescue []
|
|
166
|
-
parts << " validations: #{validations.join('; ')}" unless validations.empty?
|
|
167
|
-
|
|
168
|
-
lines << "- #{parts.join("\n")}"
|
|
169
|
-
end
|
|
170
|
-
lines.join("\n")
|
|
171
|
-
rescue => e
|
|
172
|
-
ConsoleAgent.logger.debug("ConsoleAgent: model introspection failed: #{e.message}")
|
|
173
|
-
nil
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def routes_context
|
|
177
|
-
return nil unless defined?(Rails) && Rails.respond_to?(:application)
|
|
178
|
-
|
|
179
|
-
routes = Rails.application.routes.routes
|
|
180
|
-
return nil if routes.empty?
|
|
181
|
-
|
|
182
|
-
lines = ["## Routes (summary)"]
|
|
183
|
-
count = 0
|
|
184
|
-
routes.each do |route|
|
|
185
|
-
next if route.internal?
|
|
186
|
-
path = route.path.spec.to_s.sub('(.:format)', '')
|
|
187
|
-
verb = route.verb.to_s
|
|
188
|
-
next if verb.empty?
|
|
189
|
-
action = route.defaults[:controller].to_s + '#' + route.defaults[:action].to_s
|
|
190
|
-
lines << "- #{verb} #{path} -> #{action}"
|
|
191
|
-
count += 1
|
|
192
|
-
break if count >= 50
|
|
193
|
-
end
|
|
194
|
-
lines << "- ... (#{routes.size - count} more routes)" if routes.size > count
|
|
195
|
-
|
|
196
|
-
lines.join("\n")
|
|
197
|
-
rescue => e
|
|
198
|
-
ConsoleAgent.logger.debug("ConsoleAgent: route introspection failed: #{e.message}")
|
|
199
|
-
nil
|
|
200
|
-
end
|
|
201
|
-
|
|
202
91
|
def memory_context
|
|
203
92
|
return nil unless @config.memories_enabled
|
|
204
93
|
|
|
@@ -216,14 +105,5 @@ module ConsoleAgent
|
|
|
216
105
|
nil
|
|
217
106
|
end
|
|
218
107
|
|
|
219
|
-
def eager_load_app!
|
|
220
|
-
return unless defined?(Rails) && Rails.respond_to?(:application)
|
|
221
|
-
|
|
222
|
-
if Rails.application.respond_to?(:eager_load!)
|
|
223
|
-
Rails.application.eager_load!
|
|
224
|
-
end
|
|
225
|
-
rescue => e
|
|
226
|
-
ConsoleAgent.logger.debug("ConsoleAgent: eager_load failed: #{e.message}")
|
|
227
|
-
end
|
|
228
108
|
end
|
|
229
109
|
end
|
|
@@ -3,6 +3,8 @@ require 'stringio'
|
|
|
3
3
|
module ConsoleAgent
|
|
4
4
|
# Writes to two IO streams simultaneously
|
|
5
5
|
class TeeIO
|
|
6
|
+
attr_reader :secondary
|
|
7
|
+
|
|
6
8
|
def initialize(primary, secondary)
|
|
7
9
|
@primary = primary
|
|
8
10
|
@secondary = secondary
|
|
@@ -42,6 +44,7 @@ module ConsoleAgent
|
|
|
42
44
|
CODE_REGEX = /```ruby\s*\n(.*?)```/m
|
|
43
45
|
|
|
44
46
|
attr_reader :binding_context
|
|
47
|
+
attr_accessor :on_prompt
|
|
45
48
|
|
|
46
49
|
def initialize(binding_context)
|
|
47
50
|
@binding_context = binding_context
|
|
@@ -101,6 +104,10 @@ module ConsoleAgent
|
|
|
101
104
|
@last_output
|
|
102
105
|
end
|
|
103
106
|
|
|
107
|
+
def last_answer
|
|
108
|
+
@last_answer
|
|
109
|
+
end
|
|
110
|
+
|
|
104
111
|
def last_cancelled?
|
|
105
112
|
@last_cancelled
|
|
106
113
|
end
|
|
@@ -109,8 +116,12 @@ module ConsoleAgent
|
|
|
109
116
|
return nil if code.nil? || code.strip.empty?
|
|
110
117
|
|
|
111
118
|
@last_cancelled = false
|
|
119
|
+
@last_answer = nil
|
|
112
120
|
$stdout.print colorize("Execute? [y/N/edit] ", :yellow)
|
|
121
|
+
@on_prompt&.call
|
|
113
122
|
answer = $stdin.gets.to_s.strip.downcase
|
|
123
|
+
@last_answer = answer
|
|
124
|
+
echo_stdin(answer)
|
|
114
125
|
|
|
115
126
|
case answer
|
|
116
127
|
when 'y', 'yes'
|
|
@@ -121,7 +132,9 @@ module ConsoleAgent
|
|
|
121
132
|
$stdout.puts colorize("# Edited code:", :yellow)
|
|
122
133
|
$stdout.puts highlight_code(edited)
|
|
123
134
|
$stdout.print colorize("Execute edited code? [y/N] ", :yellow)
|
|
124
|
-
|
|
135
|
+
edit_answer = $stdin.gets.to_s.strip.downcase
|
|
136
|
+
echo_stdin(edit_answer)
|
|
137
|
+
if edit_answer == 'y'
|
|
125
138
|
execute(edited)
|
|
126
139
|
else
|
|
127
140
|
$stdout.puts colorize("Cancelled.", :yellow)
|
|
@@ -139,6 +152,11 @@ module ConsoleAgent
|
|
|
139
152
|
|
|
140
153
|
private
|
|
141
154
|
|
|
155
|
+
# Write stdin input to the capture IO only (avoids double-echo on terminal)
|
|
156
|
+
def echo_stdin(text)
|
|
157
|
+
$stdout.secondary.write("#{text}\n") if $stdout.respond_to?(:secondary)
|
|
158
|
+
end
|
|
159
|
+
|
|
142
160
|
def open_in_editor(code)
|
|
143
161
|
require 'tempfile'
|
|
144
162
|
editor = ENV['EDITOR'] || 'vi'
|