rails_console_ai 0.19.0 → 0.21.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: fac547626547b2b99702978a38365e9877c11c3e5892be2d91004395ddfb92a4
4
- data.tar.gz: f89b097e49fb3fb766bc618082ace286658dce54a496decf98251c1f3a8c6eff
3
+ metadata.gz: 8faec693c3d3c83dafcaa65437081d5ba8e235f04527a28182104c4ecba5b80d
4
+ data.tar.gz: 171445a3ddc0c50093b1688260545df1558548a29cb81a6d3262d15d8ec4cf3c
5
5
  SHA512:
6
- metadata.gz: 7e9b2488043e925762da5b442412f27b92c57facf2fd880b27bbc74314c41cc4c421e091af690b262b0b6ff965aa4ad66cd5efd0bdde6e1de08737d18e7a0411
7
- data.tar.gz: fc44b20a37d2bc56254f6b4dc966225f8d7442139ae4f7270194939b5d17291e34adf4c8376edbcdbd9f6078915cdef87a293f255cb57c666213c3ad19f0d02a
6
+ metadata.gz: 1d0689ebfc82073deb58f8bd03fce39503541d63f042f531322bfc11ef0d6cf65f69fa9ae59c8b89a8a549ea9b4b7893e8505da83887ec4cbc5c1a064ffa5d99
7
+ data.tar.gz: e8f635c26035fcf73f2ccda5bd3d403a2ec33ef806c701c28f67ee8888a8189625a2569f45c369f7b29189fda3b162f03067be26590afbe5428be9da15487552
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.21.0]
6
+
7
+ - Add Slack @mention support and channel name tracking
8
+ - Stop looping after user cancels execution
9
+ - Remove edit feature from executor
10
+ - Include more info in tool call log line
11
+ - Support class methods in `bypass_guards_for_methods`
12
+ - Rename setup tasks to `ai_db_setup` and `ai_db_migrate`
13
+ - Fix effective model resolution in multi-threaded Slack bot
14
+ - Fix cost tracking with prompt caching through Bedrock
15
+ - Add `/unthink` command
16
+ - Make `!think` / `/think` thread-safe for Slack
17
+ - Fix truncating console output
18
+ - Reduce cost by deferring large output until LLM requests it
19
+ - Add `!name`, `!model`, and `/model` commands to Slack and console
20
+
21
+ ## [0.20.0]
22
+
23
+ - Add per-user system prompt seeding
24
+ - Improve explicit code execution through Slack bot
25
+ - Show last thinking output before prompting
26
+ - Add Skills system
27
+ - Add `bypass_guards_for_methods` to allow specific methods to skip safety guards
28
+ - Add `execute_code` tool for simple query execution without code fences
29
+ - Support `<code>` tags in Slack responses
30
+ - Improve Slack bot server logging and keepalive visibility
31
+ - Improve database safety guards in Rails 5
32
+
5
33
  ## [0.19.0]
6
34
 
7
35
  - Fix duplicate tool result IDs in AWS Bedrock provider
data/README.md CHANGED
@@ -12,7 +12,7 @@ irb> ai "find the 5 most recent orders over $100"
12
12
 
13
13
  Order.where("total > ?", 100).order(created_at: :desc).limit(5)
14
14
 
15
- Execute? [y/N/edit/danger] y
15
+ Execute? [y/N/danger] y
16
16
  => [#<Order id: 4821, ...>, ...]
17
17
  ```
18
18
 
@@ -62,7 +62,8 @@ end
62
62
  | `ai!` | Enter interactive mode (multi-turn conversation) |
63
63
  | `ai? "query"` | Explain only, no execution |
64
64
  | `ai_init` | Generate app guide for better AI context |
65
- | `ai_setup` | Install session logging table |
65
+ | `ai_db_setup` | Install session logging table + run migrations |
66
+ | `ai_db_migrate` | Run pending session table migrations |
66
67
  | `ai_sessions` | List recent sessions |
67
68
  | `ai_resume` | Resume a session by name or ID |
68
69
  | `ai_memories` | Show stored memories |
@@ -97,9 +98,10 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
97
98
  - **Multi-step plans** — complex tasks are broken into steps, executed sequentially with `step1`/`step2` references
98
99
  - **Two-tier models** — defaults to Sonnet for speed/cost; `/think` upgrades to Opus when you need it
99
100
  - **Cost tracking** — `/cost` shows per-model token usage and estimated spend
101
+ - **Skills** — predefined procedures with guard bypasses that the AI activates on demand
100
102
  - **Memories** — AI saves what it learns about your app across sessions
101
103
  - **App guide** — `ai_init` generates a guide injected into every system prompt
102
- - **Sessions** — name, list, and resume interactive conversations (`ai_setup` to enable)
104
+ - **Sessions** — name, list, and resume interactive conversations (`ai_db_setup` to enable)
103
105
  - **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
104
106
  - **Output trimming** — older execution outputs are automatically replaced with references; the LLM can recall them on demand via `recall_output`, and you can `/expand <id>` to see them
105
107
  - **Debug mode** — `/debug` shows context breakdown, token counts, and per-call cost estimates before and after each LLM call
@@ -141,10 +143,78 @@ Raise `RailsConsoleAi::SafetyError` in your app code to trigger the safe mode pr
141
143
  raise RailsConsoleAi::SafetyError, "Stripe charge blocked"
142
144
  ```
143
145
 
146
+ ### Allowing Specific Methods
147
+
148
+ Some operations (like admin approvals) need to write to the database even when guards are active. Use `bypass_guards_for_methods` to declare methods that should bypass all safety guards when called during an AI session:
149
+
150
+ ```ruby
151
+ RailsConsoleAi.configure do |config|
152
+ # Global — applies to all channels
153
+ config.bypass_guards_for_methods = [
154
+ 'ChangeApproval#approve_by!',
155
+ 'ChangeApproval#reject_by!'
156
+ ]
157
+
158
+ # Per-channel — only active in the specified channel
159
+ config.channels = {
160
+ 'slack' => { 'bypass_guards_for_methods' => ['Deployment#promote!'] },
161
+ 'console' => {}
162
+ }
163
+ end
164
+ ```
165
+
166
+ Global and channel-specific methods are merged for the active channel. These method shims are installed lazily on the first AI execution (not at boot) and are session-scoped — they only bypass guards inside `SafetyGuards#wrap`. Outside of an AI session (e.g. in normal web requests), the methods behave normally with zero overhead beyond a single thread-local read.
167
+
168
+ The AI is told about these trusted methods in its system prompt and will use them directly without triggering safety errors.
169
+
170
+ ### Skills
171
+
172
+ Skills bundle a step-by-step recipe with guard bypass declarations into a single file. Unlike `bypass_guards_for_methods` (which is always-on), skill bypasses are only active after the AI explicitly activates the skill.
173
+
174
+ Create markdown files in `.rails_console_ai/skills/`:
175
+
176
+ ```markdown
177
+ ---
178
+ name: Approve/Reject ChangeApprovals
179
+ description: Approve or reject change approval records on behalf of an admin
180
+ tags:
181
+ - change-approval
182
+ - admin
183
+ bypass_guards_for_methods:
184
+ - "ChangeApproval#approve_by!"
185
+ - "ChangeApproval#reject_by!"
186
+ ---
187
+
188
+ ## When to use
189
+ Use when the user asks to approve or reject a change approval.
190
+
191
+ ## Recipe
192
+ 1. Find the ChangeApproval by ID or search
193
+ 2. Confirm approve or reject
194
+ 3. Get optional review notes
195
+ 4. Determine which admin user is acting
196
+ 5. Call approve_by! or reject_by!
197
+
198
+ ## Code Examples
199
+
200
+ ca = ChangeApproval.find(id)
201
+ admin = User.find_by!(email: "admin@example.com")
202
+ ca.approve_by!(admin, "Approved per request")
203
+ ```
204
+
205
+ **How it works:**
206
+
207
+ 1. Skill summaries (name + description) appear in the AI's system prompt
208
+ 2. When the user's request matches a skill, the AI calls `activate_skill` to load the full recipe
209
+ 3. The skill's `bypass_guards_for_methods` are added to the active bypass set
210
+ 4. The AI follows the recipe, executing code with the declared methods bypassing safety guards
211
+
212
+ Skills and global `bypass_guards_for_methods` coexist — use config-level bypasses for simple trusted methods, and skills for operations that benefit from a documented procedure.
213
+
144
214
  ### Toggling Safe Mode
145
215
 
146
216
  - **`/danger`** in interactive mode toggles all guards off/on for the session
147
- - **`d`** at the `Execute? [y/N/edit/danger]` prompt disables guards for that single execution
217
+ - **`d`** at the `Execute? [y/N/danger]` prompt disables guards for that single execution
148
218
  - When a guard blocks an operation, the user is prompted: `Re-run with safe mode disabled? [y/N]`
149
219
 
150
220
  ## LLM Providers
@@ -227,7 +297,7 @@ Timeout is automatically raised to 300s minimum for local models to account for
227
297
  RailsConsoleAi.configure do |config|
228
298
  config.provider = :anthropic # :anthropic, :openai, :bedrock, :local
229
299
  config.auto_execute = false # true to skip confirmations
230
- config.session_logging = true # requires ai_setup
300
+ config.session_logging = true # requires ai_db_setup
231
301
  config.temperature = 0.2
232
302
  config.timeout = 30 # HTTP timeout in seconds
233
303
  config.max_tool_rounds = 200 # safety cap on tool-use loops
@@ -294,11 +364,14 @@ Run RailsConsoleAi as a Slack bot. Each Slack thread becomes an independent AI s
294
364
  3. **Bot Token Scopes** — OAuth & Permissions → Bot Token Scopes, add:
295
365
  - `chat:write`
296
366
  - `channels:history` (public channels)
367
+ - `channels:read` (channel names in logs, optional)
297
368
  - `groups:history` (private channels, optional)
369
+ - `groups:read` (private channel names in logs, optional)
298
370
  - `im:history` (direct messages)
299
371
  - `users:read`
300
372
 
301
373
  4. **Event Subscriptions** — Event Subscriptions → toggle ON, then under "Subscribe to bot events" add:
374
+ - `app_mention` (respond when @mentioned in any channel)
302
375
  - `message.channels` (public channels)
303
376
  - `message.groups` (private channels, optional)
304
377
  - `message.im` (direct messages)
@@ -330,7 +403,12 @@ end
330
403
  bundle exec rake rails_console_ai:slack
331
404
  ```
332
405
 
333
- This starts a long-running process (run it separately from your web server). Each new message creates a session; threaded replies continue the conversation. The bot auto-executes code with safety guards always enabled — there is no `/danger` equivalent in Slack.
406
+ This starts a long-running process (run it separately from your web server). The bot auto-executes code with safety guards always enabled — there is no `/danger` equivalent in Slack.
407
+
408
+ **@mention behavior:**
409
+ - **DMs** — the bot responds to all messages, no @mention needed.
410
+ - **Channels** — the bot only responds when @mentioned. @mention it in any channel message or thread to start a session. The person who first @mentions the bot owns the session — only they can continue the conversation, and they must @mention the bot on each message. Exception: when the bot asks a question, the owner can reply without @mentioning.
411
+ - **Joining threads** — when @mentioned mid-thread, the bot reads the thread history for context so it understands what's already been discussed.
334
412
 
335
413
  ## Requirements
336
414
 
@@ -13,6 +13,7 @@
13
13
  <tr>
14
14
  <th>Time</th>
15
15
  <th>User</th>
16
+ <th>Channel</th>
16
17
  <th>Name</th>
17
18
  <th style="max-width: 400px;">Query</th>
18
19
  <th>Mode</th>
@@ -26,6 +27,7 @@
26
27
  <tr>
27
28
  <td class="mono"><%= session.created_at.strftime('%Y-%m-%d %H:%M') %></td>
28
29
  <td><%= session.user_name %></td>
30
+ <td><%= session.try(:slack_channel_name).presence || '-' %></td>
29
31
  <td><%= session.name.present? ? session.name : '-' %></td>
30
32
  <td class="query-cell"><a href="<%= rails_console_ai.session_path(session) %>" title="<%= h session.query.truncate(200) %>"><%= truncate(session.query.gsub(/\s+/, ' ').strip, length: 80) %></a></td>
31
33
  <td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
@@ -30,6 +30,12 @@
30
30
  <label>User</label>
31
31
  <span><%= @session.user_name || '-' %></span>
32
32
  </div>
33
+ <% if @session.try(:slack_channel_name).present? %>
34
+ <div class="meta-item">
35
+ <label>Channel</label>
36
+ <span><%= @session.slack_channel_name %></span>
37
+ </div>
38
+ <% end %>
33
39
  <div class="meta-item">
34
40
  <label>Provider / Model</label>
35
41
  <span><%= @session.provider %> / <%= @session.model %></span>
@@ -76,4 +76,16 @@ RailsConsoleAi.configure do |config|
76
76
  # config.safety_guard :jobs do |&execute|
77
77
  # Sidekiq::Testing.fake! { execute.call }
78
78
  # end
79
+ #
80
+ # Bypass ALL safety guards when specific methods are called (e.g. trusted admin actions):
81
+ # config.bypass_guards_for_methods = [
82
+ # 'ChangeApproval#approve_by!',
83
+ # 'ChangeApproval#reject_by!'
84
+ # ]
85
+
86
+ # Per-channel settings (channel mode keys: 'slack', 'console'):
87
+ # config.channels = {
88
+ # 'slack' => { 'pinned_memory_tags' => ['sharding'], 'bypass_guards_for_methods' => ['ChangeApproval#approve_by!'] },
89
+ # 'console' => { 'pinned_memory_tags' => [] }
90
+ # }
79
91
  end
@@ -7,6 +7,7 @@ module RailsConsoleAi
7
7
  def display_error(text); raise NotImplementedError; end
8
8
  def display_code(code); raise NotImplementedError; end
9
9
  def display_result(text); raise NotImplementedError; end
10
+ def display_tool_call(text); end # tool call: "-> name(args)"
10
11
  def display_result_output(text); end # stdout output from code execution
11
12
  def prompt(text); raise NotImplementedError; end
12
13
  def confirm(text); raise NotImplementedError; end
@@ -18,6 +18,10 @@ module RailsConsoleAi
18
18
  $stdout.puts "\e[2m#{text}\e[0m"
19
19
  end
20
20
 
21
+ def display_tool_call(text)
22
+ $stdout.puts "\e[33m -> #{text}\e[0m"
23
+ end
24
+
21
25
  def display_warning(text)
22
26
  $stdout.puts colorize(text, :yellow)
23
27
  end
@@ -33,6 +37,33 @@ module RailsConsoleAi
33
37
  $stdout.puts
34
38
  end
35
39
 
40
+ def display_result_output(output)
41
+ text = output.to_s
42
+ return if text.strip.empty?
43
+
44
+ lines = text.lines
45
+ total_lines = lines.length
46
+ total_chars = text.length
47
+
48
+ if total_lines <= MAX_DISPLAY_LINES && total_chars <= MAX_DISPLAY_CHARS
49
+ $stdout.print text
50
+ else
51
+ truncated = lines.first(MAX_DISPLAY_LINES).join
52
+ truncated = truncated[0, MAX_DISPLAY_CHARS] if truncated.length > MAX_DISPLAY_CHARS
53
+ $stdout.print truncated
54
+
55
+ omitted_lines = [total_lines - MAX_DISPLAY_LINES, 0].max
56
+ omitted_chars = [total_chars - truncated.length, 0].max
57
+ parts = []
58
+ parts << "#{omitted_lines} lines" if omitted_lines > 0
59
+ parts << "#{omitted_chars} chars" if omitted_chars > 0
60
+
61
+ @omitted_counter += 1
62
+ @omitted_outputs[@omitted_counter] = text
63
+ $stdout.puts colorize(" (output truncated, omitting #{parts.join(', ')}) /expand #{@omitted_counter} to see all", :yellow)
64
+ end
65
+ end
66
+
36
67
  def display_result(result)
37
68
  full = "=> #{result.inspect}"
38
69
  lines = full.lines
@@ -75,7 +106,7 @@ module RailsConsoleAi
75
106
  end
76
107
 
77
108
  def mode
78
- 'interactive'
109
+ 'console'
79
110
  end
80
111
 
81
112
  def supports_editing?
@@ -251,8 +282,12 @@ module RailsConsoleAi
251
282
  @engine.display_conversation
252
283
  when '/cost'
253
284
  @engine.display_cost_summary
285
+ when '/model'
286
+ display_model_info
254
287
  when '/think'
255
288
  @engine.upgrade_to_thinking_model
289
+ when '/unthink'
290
+ @engine.downgrade_from_thinking_model
256
291
  when /\A\/expand/
257
292
  expand_id = input.sub('/expand', '').strip.to_i
258
293
  full_output = expand_output(expand_id)
@@ -316,6 +351,27 @@ module RailsConsoleAi
316
351
  end
317
352
  end
318
353
 
354
+ def display_model_info
355
+ config = RailsConsoleAi.configuration
356
+ model = @engine.effective_model
357
+ thinking = config.resolved_thinking_model
358
+ pricing = Configuration::PRICING[model]
359
+
360
+ @real_stdout.puts "\e[36m Model info:\e[0m"
361
+ @real_stdout.puts "\e[2m Provider: #{config.provider}\e[0m"
362
+ @real_stdout.puts "\e[2m Model: #{model}\e[0m"
363
+ @real_stdout.puts "\e[2m Thinking model: #{thinking}\e[0m"
364
+ @real_stdout.puts "\e[2m Max tokens: #{config.resolved_max_tokens}\e[0m"
365
+ if pricing
366
+ @real_stdout.puts "\e[2m Pricing: $#{pricing[:input] * 1_000_000}/M in, $#{pricing[:output] * 1_000_000}/M out\e[0m"
367
+ if pricing[:cache_read]
368
+ @real_stdout.puts "\e[2m Cache pricing: $#{pricing[:cache_read] * 1_000_000}/M read, $#{pricing[:cache_write] * 1_000_000}/M write\e[0m"
369
+ end
370
+ end
371
+ @real_stdout.puts "\e[2m Bedrock region: #{config.bedrock_region}\e[0m" if config.provider == :bedrock
372
+ @real_stdout.puts "\e[2m Local URL: #{config.local_url}\e[0m" if config.provider == :local
373
+ end
374
+
319
375
  def handle_name_command(input)
320
376
  name = input.sub('/name', '').strip.gsub(/\A(['"])(.*)\1\z/, '\2')
321
377
  if name.empty?
@@ -340,7 +396,9 @@ module RailsConsoleAi
340
396
  @real_stdout.puts "\e[2m /danger Toggle safe mode (currently #{safe_status})\e[0m"
341
397
  @real_stdout.puts "\e[2m /safe Show safety guard status\e[0m"
342
398
  end
399
+ @real_stdout.puts "\e[2m /model Show provider, model, and pricing info\e[0m"
343
400
  @real_stdout.puts "\e[2m /think Switch to thinking model\e[0m"
401
+ @real_stdout.puts "\e[2m /unthink Switch back to default model\e[0m"
344
402
  @real_stdout.puts "\e[2m /compact Summarize conversation to reduce context\e[0m"
345
403
  @real_stdout.puts "\e[2m /usage Show session token totals\e[0m"
346
404
  @real_stdout.puts "\e[2m /cost Show cost estimate by model\e[0m"
@@ -56,9 +56,16 @@ module RailsConsoleAi
56
56
  post(":x: #{strip_ansi(text)}")
57
57
  end
58
58
 
59
- def display_code(_code)
59
+ def display_tool_call(text)
60
+ @output_log.write("-> #{text}\n")
61
+ STDOUT.puts "#{@log_prefix} -> #{text}"
62
+ end
63
+
64
+ def display_code(code)
60
65
  # Don't post raw code/plan steps to Slack — non-technical users don't need to see Ruby
61
- nil
66
+ # But do log to STDOUT so server logs show what was generated/executed
67
+ @output_log.write("# Generated code:\n#{code}\n")
68
+ STDOUT.puts "#{@log_prefix} (code)\n# Generated code:\n#{code}"
62
69
  end
63
70
 
64
71
  def display_result_output(output)
@@ -108,6 +115,14 @@ module RailsConsoleAi
108
115
 
109
116
  You are responding to non-technical users in Slack. Follow these rules:
110
117
 
118
+ ## Code Execution
119
+ - ALWAYS use the `execute_code` tool to run Ruby code. Do NOT put code in markdown
120
+ code fences expecting it to be executed — code fences are display-only in Slack.
121
+ - Use `execute_code` for simple queries, and `execute_plan` for multi-step operations.
122
+ - If the user asks you to provide code they can run later, put it in a code fence
123
+ in your text response (it will be displayed but not executed).
124
+
125
+ ## Formatting
111
126
  - Slack does NOT support markdown tables. For tabular data, use `puts` to print
112
127
  a plain-text table inside a code block. Use fixed-width columns with padding so
113
128
  columns align. Example format:
@@ -28,7 +28,10 @@ module RailsConsoleAi
28
28
  :slack_bot_token, :slack_app_token, :slack_channel_ids, :slack_allowed_usernames,
29
29
  :local_url, :local_model, :local_api_key,
30
30
  :bedrock_region,
31
- :code_search_paths
31
+ :code_search_paths,
32
+ :channels,
33
+ :bypass_guards_for_methods,
34
+ :user_extra_info
32
35
 
33
36
  def initialize
34
37
  @provider = :anthropic
@@ -58,6 +61,14 @@ module RailsConsoleAi
58
61
  @local_api_key = nil
59
62
  @bedrock_region = nil
60
63
  @code_search_paths = %w[app]
64
+ @channels = {}
65
+ @bypass_guards_for_methods = []
66
+ @user_extra_info = {}
67
+ end
68
+
69
+ def resolve_user_extra_info(username)
70
+ return nil if @user_extra_info.nil? || @user_extra_info.empty? || username.nil?
71
+ @user_extra_info[username.to_s.downcase]
61
72
  end
62
73
 
63
74
  def safety_guards
@@ -130,10 +130,14 @@ module RailsConsoleAi
130
130
  nil
131
131
  end
132
132
 
133
- def ai_setup
133
+ def ai_db_setup
134
134
  RailsConsoleAi.setup!
135
135
  end
136
136
 
137
+ def ai_db_migrate
138
+ RailsConsoleAi.migrate!
139
+ end
140
+
137
141
  def ai_init
138
142
  require 'rails_console_ai/context_builder'
139
143
  require 'rails_console_ai/providers/base'
@@ -157,7 +161,8 @@ module RailsConsoleAi
157
161
  $stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
158
162
  $stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
159
163
  $stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
160
- $stderr.puts "\e[33m ai_setup - install session logging table\e[0m"
164
+ $stderr.puts "\e[33m ai_db_setup - install session logging table + run migrations\e[0m"
165
+ $stderr.puts "\e[33m ai_db_migrate- run pending session table migrations\e[0m"
161
166
  $stderr.puts "\e[33m ai_status - show current configuration\e[0m"
162
167
  $stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
163
168
  return nil
@@ -1,7 +1,9 @@
1
1
  module RailsConsoleAi
2
2
  class ContextBuilder
3
- def initialize(config = RailsConsoleAi.configuration)
3
+ def initialize(config = RailsConsoleAi.configuration, channel_mode: nil, user_name: nil)
4
4
  @config = config
5
+ @channel_mode = channel_mode
6
+ @user_name = user_name
5
7
  end
6
8
 
7
9
  def build
@@ -16,6 +18,10 @@ module RailsConsoleAi
16
18
  parts << smart_system_instructions
17
19
  parts << environment_context
18
20
  parts << guide_context
21
+ parts << trusted_methods_context
22
+ parts << skills_context
23
+ parts << user_extra_info_context
24
+ parts << pinned_memory_context
19
25
  parts << memory_context
20
26
  parts.compact.join("\n\n")
21
27
  end
@@ -67,18 +73,27 @@ module RailsConsoleAi
67
73
  When you use a memory, mention it briefly (e.g. "Based on what I know about sharding...").
68
74
  When you discover important patterns about this app, save them as memories.
69
75
 
70
- You have an execute_plan tool to run multi-step code. When a task requires multiple
71
- sequential operations, use execute_plan with an array of steps (each with a description
72
- and Ruby code). The plan is shown to the user for review before execution begins.
73
- After each step runs, its return value is stored as step1, step2, etc. use these
74
- variables in later steps to reference earlier results (e.g. `api = SalesforceApi.new(step1)`).
75
- For simple single-expression answers, you may respond with a ```ruby code block instead.
76
+ You have tools for executing Ruby code:
77
+ - Use execute_code for simple queries and single operations.
78
+ - Use execute_plan for multi-step tasks that require sequential operations. Each step
79
+ has a description and Ruby code. The plan is shown to the user for review before
80
+ execution begins. After each step runs, its return value is stored as step1, step2,
81
+ etc. use these variables in later steps to reference earlier results
82
+ (e.g. `api = SalesforceApi.new(step1)`).
83
+ - If the user asks you to provide code for them to run later (not execute now), put it
84
+ in a ```ruby code block in your text response.
85
+ - Use `RailsConsoleAi.configuration.safety_guards.without_guards { }` to wrap any
86
+ operation that should bypass safety guards (e.g. calling a known-safe admin method).
87
+
88
+ You have skills — predefined procedures for specific operations. When a user's request
89
+ matches a skill, call activate_skill first to load the recipe and enable its guard
90
+ bypasses, then follow the recipe.
76
91
 
77
92
  RULES:
78
93
  - Give ONE concise answer. Do not offer multiple alternatives or variations.
79
94
  - For multi-step tasks, use execute_plan to break the work into small, clear steps.
80
- - For simple queries, respond with a single ```ruby code block.
81
- - Include a brief one-line explanation before any code block.
95
+ - For simple queries, use the execute_code tool.
96
+ - Include a brief one-line explanation before or after executing code.
82
97
  - Use the app's actual model names, associations, and schema.
83
98
  - Prefer ActiveRecord query interface over raw SQL.
84
99
  - For destructive operations, add a comment warning.
@@ -89,6 +104,19 @@ module RailsConsoleAi
89
104
  PROMPT
90
105
  end
91
106
 
107
+ def trusted_methods_context
108
+ methods = Array(@config.bypass_guards_for_methods)
109
+ if @channel_mode
110
+ channel_cfg = @config.channels[@channel_mode] || {}
111
+ methods = methods | Array(channel_cfg['bypass_guards_for_methods'])
112
+ end
113
+ return nil if methods.empty?
114
+
115
+ lines = ["## Trusted Methods (safety guards bypassed automatically)"]
116
+ methods.each { |m| lines << "- #{m}" }
117
+ lines.join("\n")
118
+ end
119
+
92
120
  def guide_context
93
121
  content = RailsConsoleAi.storage.read(RailsConsoleAi::GUIDE_KEY)
94
122
  return nil if content.nil? || content.strip.empty?
@@ -99,6 +127,48 @@ module RailsConsoleAi
99
127
  nil
100
128
  end
101
129
 
130
+ def skills_context
131
+ require 'rails_console_ai/skill_loader'
132
+ summaries = RailsConsoleAi::SkillLoader.new.skill_summaries
133
+ return nil if summaries.nil? || summaries.empty?
134
+
135
+ lines = ["## Skills (call activate_skill to use)"]
136
+ lines.concat(summaries)
137
+ lines.join("\n")
138
+ rescue => e
139
+ RailsConsoleAi.logger.debug("RailsConsoleAi: skills context failed: #{e.message}")
140
+ nil
141
+ end
142
+
143
+ def user_extra_info_context
144
+ info = @config.resolve_user_extra_info(@user_name)
145
+ return nil if info.nil? || info.strip.empty?
146
+
147
+ "## Current User\n\nUser: #{@user_name}\n#{info}"
148
+ end
149
+
150
+ def pinned_memory_context
151
+ return nil unless @channel_mode
152
+
153
+ channel_cfg = @config.channels[@channel_mode] || {}
154
+ pinned_tags = channel_cfg['pinned_memory_tags'] || []
155
+ return nil if pinned_tags.empty?
156
+
157
+ require 'rails_console_ai/tools/memory_tools'
158
+ sections = pinned_tags.filter_map do |tag|
159
+ content = Tools::MemoryTools.new.recall_memories(tag: tag)
160
+ next if content.nil? || content.include?("No memories")
161
+ content
162
+ end
163
+ return nil if sections.empty?
164
+
165
+ "## Pinned Memories (always available — no need to recall_memories for these)\n\n" \
166
+ + sections.join("\n\n")
167
+ rescue => e
168
+ RailsConsoleAi.logger.debug("RailsConsoleAi: pinned memory context failed: #{e.message}")
169
+ nil
170
+ end
171
+
102
172
  def memory_context
103
173
  return nil unless @config.memories_enabled
104
174