rails_console_ai 0.14.0 → 0.16.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/CHANGELOG.md +11 -0
- data/README.md +14 -1
- data/lib/rails_console_ai/channel/slack.rb +13 -27
- data/lib/rails_console_ai/configuration.rb +3 -1
- data/lib/rails_console_ai/executor.rb +2 -2
- data/lib/rails_console_ai/tools/code_tools.rb +36 -18
- data/lib/rails_console_ai/tools/registry.rb +4 -4
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e038cc77de185b7afbbc1d859400948335adfe8d69e714ef35d57a8c49fb8b34
|
|
4
|
+
data.tar.gz: e614a1ccaa212bfaa93dfb1770f3d8c5b39ee2c1e678ba3a4cab3786f7e0c86f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba68a62e707d3b35f2fc0f7f24d9d73d930c951936f37f8ec60c34dce7975bf67e184bf05fd418db1773c9ec7d58455cf25c122477ec67bde8fcee47247fcb25
|
|
7
|
+
data.tar.gz: 3fa2a64467e090b52ae68b21f93e9a1cc15f03b445f8f9a02ec0ca51d74514211f6a4eeff551ec91858adccb7c59e1a46218191480c7e749c28538c665897287
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.16.0]
|
|
6
|
+
|
|
7
|
+
- Run migrations during setup
|
|
8
|
+
- Clarify LLM activity messages in Slack channel
|
|
9
|
+
- Clean up error messages
|
|
10
|
+
- Rotate thinking messages in Slack bot
|
|
11
|
+
|
|
12
|
+
## [0.15.0]
|
|
13
|
+
|
|
14
|
+
- Add `config.code_search_paths` to configure searchable code directories
|
|
15
|
+
|
|
5
16
|
## [0.14.0]
|
|
6
17
|
|
|
7
18
|
- Change module name from `RailsConsoleAI` to `RailsConsoleAi`
|
data/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
|
|
|
93
93
|
|
|
94
94
|
## Features
|
|
95
95
|
|
|
96
|
-
- **Tool use** — AI introspects your schema, models, files, and code to write accurate queries
|
|
96
|
+
- **Tool use** — AI introspects your schema, models, files (Ruby, ERB, HTML, JS, CSS, YAML, etc), and code to write accurate queries
|
|
97
97
|
- **Multi-step plans** — complex tasks are broken into steps, executed sequentially with `step1`/`step2` references
|
|
98
98
|
- **Two-tier models** — defaults to Sonnet for speed/cost; `/think` upgrades to Opus when you need it
|
|
99
99
|
- **Cost tracking** — `/cost` shows per-model token usage and estimated spend
|
|
@@ -231,9 +231,22 @@ RailsConsoleAi.configure do |config|
|
|
|
231
231
|
config.temperature = 0.2
|
|
232
232
|
config.timeout = 30 # HTTP timeout in seconds
|
|
233
233
|
config.max_tool_rounds = 200 # safety cap on tool-use loops
|
|
234
|
+
config.code_search_paths = %w[app] # directories for list_files / search_code
|
|
234
235
|
end
|
|
235
236
|
```
|
|
236
237
|
|
|
238
|
+
### Code Search Paths
|
|
239
|
+
|
|
240
|
+
By default, `list_files` and `search_code` only look in `app/`. If your project has code in other directories (e.g. a frontend in `public/portal`, or shared code in `lib`), add them:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
RailsConsoleAi.configure do |config|
|
|
244
|
+
config.code_search_paths = %w[app lib public/portal]
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The tools search all configured paths when no explicit directory is passed. You can still pass a specific directory to either tool to override this.
|
|
249
|
+
|
|
237
250
|
## Web UI Authentication
|
|
238
251
|
|
|
239
252
|
The engine mounts a session viewer at `/rails_console_ai`. By default it's open — you can protect it with basic auth or a custom authentication function.
|
|
@@ -5,24 +5,6 @@ module RailsConsoleAi
|
|
|
5
5
|
class Slack < Base
|
|
6
6
|
ANSI_REGEX = /\e\[[0-9;]*m/
|
|
7
7
|
|
|
8
|
-
THINKING_MESSAGES = [
|
|
9
|
-
"Thinking...",
|
|
10
|
-
"Reticulating splines...",
|
|
11
|
-
"Scrubbing encryption bits...",
|
|
12
|
-
"Consulting the oracle...",
|
|
13
|
-
"Rummaging through the database...",
|
|
14
|
-
"Warming up the hamster wheel...",
|
|
15
|
-
"Polishing the pixels...",
|
|
16
|
-
"Untangling the spaghetti code...",
|
|
17
|
-
"Asking the magic 8-ball...",
|
|
18
|
-
"Counting all the things...",
|
|
19
|
-
"Herding the electrons...",
|
|
20
|
-
"Dusting off the old records...",
|
|
21
|
-
"Feeding the algorithms...",
|
|
22
|
-
"Shaking the data tree...",
|
|
23
|
-
"Bribing the servers...",
|
|
24
|
-
].freeze
|
|
25
|
-
|
|
26
8
|
def initialize(slack_bot:, channel_id:, thread_ts:, user_name: nil)
|
|
27
9
|
@slack_bot = slack_bot
|
|
28
10
|
@channel_id = channel_id
|
|
@@ -47,13 +29,20 @@ module RailsConsoleAi
|
|
|
47
29
|
end
|
|
48
30
|
|
|
49
31
|
def display_dim(text)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
raw = strip_ansi(text)
|
|
33
|
+
stripped = raw.strip
|
|
34
|
+
|
|
35
|
+
if stripped =~ /\AThinking\.\.\.|\AAttempting to fix|\ACancelled|\A_session:/
|
|
36
|
+
post(stripped)
|
|
37
|
+
elsif stripped =~ /\ACalling LLM/
|
|
38
|
+
# Technical LLM round status — suppress in Slack
|
|
39
|
+
@output_log.write("#{stripped}\n")
|
|
40
|
+
$stdout.puts "#{@log_prefix} (dim) #{stripped}"
|
|
41
|
+
elsif raw =~ /\A {2,4}\S/ && stripped.length > 10
|
|
42
|
+
# LLM thinking text (2-space indent from conversation engine) — show as status
|
|
54
43
|
post(stripped)
|
|
55
44
|
else
|
|
56
|
-
#
|
|
45
|
+
# Tool result previews (5+ space indent) and other technical noise — log only
|
|
57
46
|
@output_log.write("#{stripped}\n")
|
|
58
47
|
$stdout.puts "#{@log_prefix} (dim) #{stripped}"
|
|
59
48
|
end
|
|
@@ -135,6 +124,7 @@ module RailsConsoleAi
|
|
|
135
124
|
- The output of `puts` in your code is automatically shown to the user. Do NOT
|
|
136
125
|
repeat or re-display data that your code already printed via `puts`.
|
|
137
126
|
Just add a brief summary after (e.g. "10 events found" or "Let me know if you need more detail").
|
|
127
|
+
- Do not offer to make changes or take actions on behalf of the user. Only report findings.
|
|
138
128
|
- This is a live production database — other processes, users, and background jobs are
|
|
139
129
|
constantly changing data. Never assume results will be the same as a previous query.
|
|
140
130
|
Always re-run queries when asked, even if you just ran the same one.
|
|
@@ -170,10 +160,6 @@ module RailsConsoleAi
|
|
|
170
160
|
RailsConsoleAi.logger.error("Slack post failed: #{e.message}")
|
|
171
161
|
end
|
|
172
162
|
|
|
173
|
-
def random_thinking_message
|
|
174
|
-
THINKING_MESSAGES.sample
|
|
175
|
-
end
|
|
176
|
-
|
|
177
163
|
def strip_ansi(text)
|
|
178
164
|
text.to_s.gsub(ANSI_REGEX, '')
|
|
179
165
|
end
|
|
@@ -27,7 +27,8 @@ module RailsConsoleAi
|
|
|
27
27
|
:authenticate,
|
|
28
28
|
:slack_bot_token, :slack_app_token, :slack_channel_ids, :slack_allowed_usernames,
|
|
29
29
|
:local_url, :local_model, :local_api_key,
|
|
30
|
-
:bedrock_region
|
|
30
|
+
:bedrock_region,
|
|
31
|
+
:code_search_paths
|
|
31
32
|
|
|
32
33
|
def initialize
|
|
33
34
|
@provider = :anthropic
|
|
@@ -56,6 +57,7 @@ module RailsConsoleAi
|
|
|
56
57
|
@local_model = 'qwen2.5:7b'
|
|
57
58
|
@local_api_key = nil
|
|
58
59
|
@bedrock_region = nil
|
|
60
|
+
@code_search_paths = %w[app]
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
def safety_guards
|
|
@@ -140,8 +140,8 @@ module RailsConsoleAi
|
|
|
140
140
|
return nil
|
|
141
141
|
end
|
|
142
142
|
@last_error = "#{e.class}: #{e.message}"
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
backtrace = e.backtrace.first(3).map { |line| " #{line}" }.join("\n")
|
|
144
|
+
display_error("Error: #{@last_error}\n#{backtrace}")
|
|
145
145
|
@last_output = captured_output&.string
|
|
146
146
|
nil
|
|
147
147
|
end
|
|
@@ -4,23 +4,31 @@ module RailsConsoleAi
|
|
|
4
4
|
MAX_FILE_LINES = 500
|
|
5
5
|
MAX_LIST_ENTRIES = 100
|
|
6
6
|
MAX_SEARCH_RESULTS = 50
|
|
7
|
+
FILE_EXTENSIONS = %w[rb erb haml slim html js jsx ts tsx css scss sass json yml yaml rake].freeze
|
|
8
|
+
FILE_GLOB = "*.{#{FILE_EXTENSIONS.join(',')}}".freeze
|
|
7
9
|
|
|
8
10
|
def list_files(directory = nil)
|
|
9
|
-
directory = sanitize_directory(directory || 'app')
|
|
10
11
|
root = rails_root
|
|
11
12
|
return "Rails.root is not available." unless root
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
return "Directory '#{directory}' not found." unless File.directory?(full_path)
|
|
14
|
+
directories = directory ? [sanitize_directory(directory)] : default_search_paths
|
|
15
15
|
|
|
16
|
-
files =
|
|
17
|
-
|
|
16
|
+
files = []
|
|
17
|
+
directories.each do |dir|
|
|
18
|
+
full_path = File.join(root, dir)
|
|
19
|
+
next unless File.directory?(full_path)
|
|
20
|
+
|
|
21
|
+
Dir.glob(File.join(full_path, '**', FILE_GLOB)).sort.each do |f|
|
|
22
|
+
files << f.sub("#{root}/", '')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
18
25
|
|
|
19
26
|
if files.length > MAX_LIST_ENTRIES
|
|
20
27
|
truncated = files.first(MAX_LIST_ENTRIES)
|
|
21
28
|
truncated.join("\n") + "\n... and #{files.length - MAX_LIST_ENTRIES} more files"
|
|
22
29
|
elsif files.empty?
|
|
23
|
-
|
|
30
|
+
searched = directory || default_search_paths.join(', ')
|
|
31
|
+
"No files found in '#{searched}'."
|
|
24
32
|
else
|
|
25
33
|
files.join("\n")
|
|
26
34
|
end
|
|
@@ -71,30 +79,36 @@ module RailsConsoleAi
|
|
|
71
79
|
def search_code(query, directory = nil)
|
|
72
80
|
return "Error: query is required." if query.nil? || query.strip.empty?
|
|
73
81
|
|
|
74
|
-
directory = sanitize_directory(directory || 'app')
|
|
75
82
|
root = rails_root
|
|
76
83
|
return "Rails.root is not available." unless root
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
return "Directory '#{directory}' not found." unless File.directory?(full_path)
|
|
85
|
+
directories = directory ? [sanitize_directory(directory)] : default_search_paths
|
|
80
86
|
|
|
81
87
|
results = []
|
|
82
|
-
|
|
88
|
+
directories.each do |dir|
|
|
83
89
|
break if results.length >= MAX_SEARCH_RESULTS
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
File.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
full_path = File.join(root, dir)
|
|
92
|
+
next unless File.directory?(full_path)
|
|
93
|
+
|
|
94
|
+
Dir.glob(File.join(full_path, '**', FILE_GLOB)).sort.each do |file|
|
|
95
|
+
break if results.length >= MAX_SEARCH_RESULTS
|
|
96
|
+
|
|
97
|
+
relative = file.sub("#{root}/", '')
|
|
98
|
+
File.readlines(file).each_with_index do |line, idx|
|
|
99
|
+
if line.include?(query)
|
|
100
|
+
results << "#{relative}:#{idx + 1}: #{line.strip}"
|
|
101
|
+
break if results.length >= MAX_SEARCH_RESULTS
|
|
102
|
+
end
|
|
90
103
|
end
|
|
104
|
+
rescue => e
|
|
105
|
+
# skip unreadable files
|
|
91
106
|
end
|
|
92
|
-
rescue => e
|
|
93
|
-
# skip unreadable files
|
|
94
107
|
end
|
|
95
108
|
|
|
96
109
|
if results.empty?
|
|
97
|
-
|
|
110
|
+
searched = directory || default_search_paths.join(', ')
|
|
111
|
+
"No matches found for '#{query}' in #{searched}."
|
|
98
112
|
else
|
|
99
113
|
header = "Found #{results.length} match#{'es' if results.length != 1}:\n"
|
|
100
114
|
header + results.join("\n")
|
|
@@ -113,6 +127,10 @@ module RailsConsoleAi
|
|
|
113
127
|
end
|
|
114
128
|
end
|
|
115
129
|
|
|
130
|
+
def default_search_paths
|
|
131
|
+
RailsConsoleAi.configuration.code_search_paths
|
|
132
|
+
end
|
|
133
|
+
|
|
116
134
|
def sanitize_path(path)
|
|
117
135
|
# Remove leading slashes and ../ sequences
|
|
118
136
|
path.strip.gsub(/\A\/+/, '').gsub(/\.\.\//, '').gsub(/\.\.\\/, '')
|
|
@@ -144,11 +144,11 @@ module RailsConsoleAi
|
|
|
144
144
|
|
|
145
145
|
register(
|
|
146
146
|
name: 'list_files',
|
|
147
|
-
description:
|
|
147
|
+
description: "List files in this Rails app (Ruby, ERB, HTML, JS, CSS, YAML, etc). Searches configured paths by default: #{RailsConsoleAi.configuration.code_search_paths.join(', ')}.",
|
|
148
148
|
parameters: {
|
|
149
149
|
'type' => 'object',
|
|
150
150
|
'properties' => {
|
|
151
|
-
'directory' => { 'type' => 'string', 'description' => 'Relative directory path (e.g. "app/models", "lib").
|
|
151
|
+
'directory' => { 'type' => 'string', 'description' => 'Relative directory path (e.g. "app/models", "lib"). Omit to search all configured paths.' }
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
154
|
handler: ->(args) { code.list_files(args['directory']) }
|
|
@@ -171,12 +171,12 @@ module RailsConsoleAi
|
|
|
171
171
|
|
|
172
172
|
register(
|
|
173
173
|
name: 'search_code',
|
|
174
|
-
description:
|
|
174
|
+
description: "Search for a pattern in project files (Ruby, ERB, HTML, JS, CSS, YAML, etc). Returns matching lines with file paths. Searches configured paths by default: #{RailsConsoleAi.configuration.code_search_paths.join(', ')}.",
|
|
175
175
|
parameters: {
|
|
176
176
|
'type' => 'object',
|
|
177
177
|
'properties' => {
|
|
178
178
|
'query' => { 'type' => 'string', 'description' => 'Search pattern (substring match)' },
|
|
179
|
-
'directory' => { 'type' => 'string', 'description' => 'Relative directory to search in.
|
|
179
|
+
'directory' => { 'type' => 'string', 'description' => 'Relative directory to search in. Omit to search all configured paths.' }
|
|
180
180
|
},
|
|
181
181
|
'required' => ['query']
|
|
182
182
|
},
|
data/lib/rails_console_ai.rb
CHANGED
|
@@ -94,7 +94,8 @@ module RailsConsoleAi
|
|
|
94
94
|
table = 'rails_console_ai_sessions'
|
|
95
95
|
|
|
96
96
|
if conn.table_exists?(table)
|
|
97
|
-
$stdout.puts "\e[32mRailsConsoleAi: #{table} already exists
|
|
97
|
+
$stdout.puts "\e[32mRailsConsoleAi: #{table} already exists — checking for pending migrations.\e[0m"
|
|
98
|
+
migrate!
|
|
98
99
|
else
|
|
99
100
|
conn.create_table(table) do |t|
|
|
100
101
|
t.text :query, null: false
|
|
@@ -151,6 +152,7 @@ module RailsConsoleAi
|
|
|
151
152
|
if migrations.empty?
|
|
152
153
|
$stdout.puts "\e[32mRailsConsoleAi: #{table} is up to date.\e[0m"
|
|
153
154
|
else
|
|
155
|
+
RailsConsoleAi::Session.reset_column_information if defined?(RailsConsoleAi::Session)
|
|
154
156
|
$stdout.puts "\e[32mRailsConsoleAi: added columns: #{migrations.join(', ')}.\e[0m"
|
|
155
157
|
end
|
|
156
158
|
rescue => e
|