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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85382da9107682f9e96af6cfe8abc37f83e72e09bba910c825892c8705537675
4
- data.tar.gz: 191afb47e155da33c711bd1d69a04a07ad2e10c256d9cfb5e8b2bf09f04cb3b8
3
+ metadata.gz: e038cc77de185b7afbbc1d859400948335adfe8d69e714ef35d57a8c49fb8b34
4
+ data.tar.gz: e614a1ccaa212bfaa93dfb1770f3d8c5b39ee2c1e678ba3a4cab3786f7e0c86f
5
5
  SHA512:
6
- metadata.gz: 836313ba09d65cd8dff17afe058b3c249c969171da9d1daf8644c519a41640289f86223f9d21ef63d1fdde7286bddf6545bbf70a6f338265d68f8822e3da3a82
7
- data.tar.gz: 58041e55f589fb87a380516617934fcbcaa13fed0d176284335dfe2ad41491d958a1ffb24beb4a9392a6b76fbbec118faf00a9b0fa48ee38ca7e99eb08b767f7
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
- stripped = strip_ansi(text).strip
51
- if stripped =~ /\AThinking\.\.\.|\ACalling LLM/
52
- post(random_thinking_message)
53
- elsif stripped =~ /\AAttempting to fix|\ACancelled|\A_session:/
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
- # Log for engineers but don't post to Slack
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
- display_error("Error: #{@last_error}")
144
- e.backtrace.first(3).each { |line| display_error(" #{line}") }
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
- full_path = File.join(root, directory)
14
- return "Directory '#{directory}' not found." unless File.directory?(full_path)
14
+ directories = directory ? [sanitize_directory(directory)] : default_search_paths
15
15
 
16
- files = Dir.glob(File.join(full_path, '**', '*.rb')).sort
17
- files = files.map { |f| f.sub("#{root}/", '') }
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
- "No Ruby files found in '#{directory}'."
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
- full_path = File.join(root, directory)
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
- Dir.glob(File.join(full_path, '**', '*.rb')).sort.each do |file|
88
+ directories.each do |dir|
83
89
  break if results.length >= MAX_SEARCH_RESULTS
84
90
 
85
- relative = file.sub("#{root}/", '')
86
- File.readlines(file).each_with_index do |line, idx|
87
- if line.include?(query)
88
- results << "#{relative}:#{idx + 1}: #{line.strip}"
89
- break if results.length >= MAX_SEARCH_RESULTS
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
- "No matches found for '#{query}' in #{directory}/."
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: 'List Ruby files in a directory of this Rails app. Defaults to app/ directory.',
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"). Defaults to "app".' }
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: 'Search for a pattern in Ruby files. Returns matching lines with file paths.',
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. Defaults to "app".' }
179
+ 'directory' => { 'type' => 'string', 'description' => 'Relative directory to search in. Omit to search all configured paths.' }
180
180
  },
181
181
  'required' => ['query']
182
182
  },
@@ -1,3 +1,3 @@
1
1
  module RailsConsoleAi
2
- VERSION = '0.14.0'.freeze
2
+ VERSION = '0.16.0'.freeze
3
3
  end
@@ -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. Run RailsConsoleAi.teardown! first to recreate.\e[0m"
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_console_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr