console_agent 0.10.0 → 0.11.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: dc46d0592feb84b4d85481d1535dccbe417a4445593828424c12a84d96fcbc9c
4
- data.tar.gz: 10fe29dc81cc425a498c6e7d6c6b82aaa586ec674c081ba3f7b5b1143b68df18
3
+ metadata.gz: 902a34c15b9fa06e60e4b8eb040d57126bbdf9f725d2e727634ed4e73d8fbaa0
4
+ data.tar.gz: cda463c1023ad995a8143d52702a6b8098dbcdd3cde4e2002352c327a536109e
5
5
  SHA512:
6
- metadata.gz: 86760d6c3b7c4920fc2c01741be308fc3d3f133e264c8dc37cab6b1ab90e9b920a410d57c86d8f96e743396d6919735d7fad62ee584667c8ea177c4825a12d05
7
- data.tar.gz: 6446b9b2af4803ccd860fd109484ef37de87850517ad117eb52892974a65a017c1b03188dfc1eb7f24aad3859749ce4f6d282220c8d5d78238e67cfdb7438def
6
+ metadata.gz: 6380481f86f277205aa2aadac23745cf7f5fe5785b8f5ce8272c64fd26206166814434f06bbcf97494f6f1d75a08e69657721fd5423e62898a7c1e5d7312347f
7
+ data.tar.gz: adffc1e8afa7ad60469c03df75427284c988c8b1e64f954571435441397e66cd3d597906feb2fa953e19575432b27b7583bfcea0e73bc4cbc49cdd19e32bc2d0
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.11.0]
6
+
7
+ - Add Slack channel integration with system instructions and connection pooling
8
+ - Extract channels abstraction and ConversationEngine from Repl
9
+ - Add built-in safety guards with `/danger` bypass and progressive safety allowlists
10
+ - Add local model support with prompt truncation warnings
11
+ - Add `clear!` command to clear bot messages in thread
12
+ - Match code blocks in LLM results
13
+ - Fix long query display and add cost tracking to session viewer
14
+ - Strip quotes from session names when saving
15
+
5
16
  ## [0.10.0]
6
17
 
7
18
  - Add `/expand` command to view previous results
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] y
15
+ Execute? [y/N/edit/danger] y
16
16
  => [#<Order id: 4821, ...>, ...]
17
17
  ```
18
18
 
@@ -75,6 +75,8 @@ end
75
75
  | Command | What it does |
76
76
  |---------|-------------|
77
77
  | `/auto` | Toggle auto-execute (skip confirmations) |
78
+ | `/danger` | Toggle safe mode off/on (allow side effects) |
79
+ | `/safe` | Show safety guard status |
78
80
  | `/compact` | Compress history into a summary (saves tokens) |
79
81
  | `/usage` | Show token stats |
80
82
  | `/cost` | Show per-model cost breakdown |
@@ -101,6 +103,49 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
101
103
  - **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
102
104
  - **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
103
105
  - **Debug mode** — `/debug` shows context breakdown, token counts, and per-call cost estimates before and after each LLM call
106
+ - **Safe mode** — configurable guards that block side effects (DB writes, HTTP mutations, email delivery) during AI code execution
107
+
108
+ ## Safety Guards
109
+
110
+ Safety guards prevent AI-generated code from causing side effects. When a guard blocks an operation, the user is prompted to re-run with safe mode disabled.
111
+
112
+ ### Built-in Guards
113
+
114
+ ```ruby
115
+ ConsoleAgent.configure do |config|
116
+ config.use_builtin_safety_guard :database_writes # blocks INSERT/UPDATE/DELETE/DROP/etc.
117
+ config.use_builtin_safety_guard :http_mutations # blocks POST/PUT/PATCH/DELETE via Net::HTTP
118
+ config.use_builtin_safety_guard :mailers # disables ActionMailer delivery
119
+ end
120
+ ```
121
+
122
+ - **`:database_writes`** — intercepts the ActiveRecord connection adapter to block write SQL. Works on Rails 5+ with any database adapter.
123
+ - **`:http_mutations`** — intercepts `Net::HTTP#request` to block non-GET/HEAD/OPTIONS requests. Covers libraries built on Net::HTTP (HTTParty, RestClient, Faraday).
124
+ - **`:mailers`** — sets `ActionMailer::Base.perform_deliveries = false` during execution.
125
+
126
+ ### Custom Guards
127
+
128
+ Write your own guards using the around-block pattern:
129
+
130
+ ```ruby
131
+ ConsoleAgent.configure do |config|
132
+ config.safety_guard :jobs do |&execute|
133
+ Sidekiq::Testing.fake! { execute.call }
134
+ end
135
+ end
136
+ ```
137
+
138
+ Raise `ConsoleAgent::SafetyError` in your app code to trigger the safe mode prompt:
139
+
140
+ ```ruby
141
+ raise ConsoleAgent::SafetyError, "Stripe charge blocked"
142
+ ```
143
+
144
+ ### Toggling Safe Mode
145
+
146
+ - **`/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
148
+ - When a guard blocks an operation, the user is prompted: `Re-run with safe mode disabled? [y/N]`
104
149
 
105
150
  ## Configuration
106
151
 
@@ -146,6 +191,58 @@ end
146
191
 
147
192
  When `authenticate` is set, `admin_username` / `admin_password` are ignored.
148
193
 
194
+ ## Channels
195
+
196
+ ConsoleAgent can run through different channels beyond the Rails console. Each channel is a separate process that connects the same AI engine to a different interface.
197
+
198
+ ### Slack
199
+
200
+ Run ConsoleAgent as a Slack bot. Each Slack thread becomes an independent AI session with full tool use, multi-step plans, and safety guards always on.
201
+
202
+ #### Slack App Setup
203
+
204
+ 1. Create a new app at https://api.slack.com/apps → **Create New App** → **From scratch**
205
+
206
+ 2. **Enable Socket Mode** — Settings → Socket Mode → toggle ON. Generate an App-Level Token with the `connections:write` scope. Copy the `xapp-...` token.
207
+
208
+ 3. **Bot Token Scopes** — OAuth & Permissions → Bot Token Scopes, add:
209
+ - `chat:write`
210
+ - `channels:history` (public channels)
211
+ - `groups:history` (private channels, optional)
212
+ - `im:history` (direct messages)
213
+ - `users:read`
214
+
215
+ 4. **Event Subscriptions** — Event Subscriptions → toggle ON, then under "Subscribe to bot events" add:
216
+ - `message.channels` (public channels)
217
+ - `message.groups` (private channels, optional)
218
+ - `message.im` (direct messages)
219
+
220
+ 5. **App Home** — Show Tabs → toggle **Messages Tab** ON and check **"Allow users to send Slash commands and messages from the messages tab"**
221
+
222
+ 6. **Install to workspace** — Install App → Install to Workspace. Copy the `xoxb-...` Bot User OAuth Token.
223
+
224
+ 7. **Invite the bot** to a channel with `/invite @YourBotName`, or DM it directly.
225
+
226
+ #### Configuration
227
+
228
+ ```ruby
229
+ ConsoleAgent.configure do |config|
230
+ config.slack_bot_token = ENV['SLACK_BOT_TOKEN'] # xoxb-...
231
+ config.slack_app_token = ENV['SLACK_APP_TOKEN'] # xapp-...
232
+
233
+ # Optional: restrict to specific Slack channel IDs
234
+ # config.slack_channel_ids = 'C1234567890,C0987654321'
235
+ end
236
+ ```
237
+
238
+ #### Running
239
+
240
+ ```bash
241
+ bundle exec rake console_agent:slack
242
+ ```
243
+
244
+ 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.
245
+
149
246
  ## Requirements
150
247
 
151
248
  Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
@@ -1,5 +1,19 @@
1
1
  module ConsoleAgent
2
2
  module SessionsHelper
3
+ def estimated_cost(session)
4
+ pricing = Configuration::PRICING[session.model]
5
+ return nil unless pricing
6
+
7
+ (session.input_tokens * pricing[:input]) + (session.output_tokens * pricing[:output])
8
+ end
9
+
10
+ def format_cost(session)
11
+ cost = estimated_cost(session)
12
+ return '-' unless cost
13
+
14
+ cost < 0.01 ? "<$0.01" : "$#{'%.2f' % cost}"
15
+ end
16
+
3
17
  # Convert ANSI escape codes to HTML spans for terminal-style rendering
4
18
  def ansi_to_html(text)
5
19
  return '' if text.nil? || text.empty?
@@ -4,7 +4,7 @@ module ConsoleAgent
4
4
 
5
5
  validates :query, presence: true
6
6
  validates :conversation, presence: true
7
- validates :mode, presence: true, inclusion: { in: %w[one_shot interactive explain] }
7
+ validates :mode, presence: true, inclusion: { in: %w[one_shot interactive explain slack] }
8
8
 
9
9
  scope :recent, -> { order(created_at: :desc) }
10
10
  scope :named, ->(name) { where(name: name) }
@@ -14,10 +14,10 @@
14
14
  <th>Time</th>
15
15
  <th>User</th>
16
16
  <th>Name</th>
17
- <th>Query</th>
17
+ <th style="max-width: 400px;">Query</th>
18
18
  <th>Mode</th>
19
19
  <th>Tokens</th>
20
- <th>Executed?</th>
20
+ <th>Cost</th>
21
21
  <th>Duration</th>
22
22
  </tr>
23
23
  </thead>
@@ -27,10 +27,10 @@
27
27
  <td class="mono"><%= session.created_at.strftime('%Y-%m-%d %H:%M') %></td>
28
28
  <td><%= session.user_name %></td>
29
29
  <td><%= session.name.present? ? session.name : '-' %></td>
30
- <td><a href="<%= console_agent.session_path(session) %>"><%= truncate(session.query, length: 80) %></a></td>
30
+ <td class="query-cell"><a href="<%= console_agent.session_path(session) %>" title="<%= h session.query.truncate(200) %>"><%= truncate(session.query.gsub(/\s+/, ' ').strip, length: 80) %></a></td>
31
31
  <td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
32
32
  <td class="mono"><%= session.input_tokens + session.output_tokens %></td>
33
- <td><%= session.executed? ? '<span class="check">Yes</span>'.html_safe : '<span class="cross">No</span>'.html_safe %></td>
33
+ <td class="mono"><%= format_cost(session) %></td>
34
34
  <td class="mono"><%= session.duration_ms ? "#{session.duration_ms}ms" : '-' %></td>
35
35
  </tr>
36
36
  <% end %>
@@ -2,9 +2,19 @@
2
2
 
3
3
  <div class="meta-card">
4
4
  <div class="meta-grid">
5
- <div class="meta-item">
5
+ <div class="meta-item" style="grid-column: 1 / -1;">
6
6
  <label>Query</label>
7
- <span><%= @session.query %></span>
7
+ <% if @session.query.length > 300 %>
8
+ <div class="query-preview">
9
+ <span><%= truncate(@session.query, length: 300) %></span>
10
+ <details style="margin-top: 4px;">
11
+ <summary style="cursor: pointer; font-size: 12px; color: #4a6fa5;">Show full query</summary>
12
+ <pre style="white-space: pre-wrap; word-wrap: break-word; font-size: 13px; margin-top: 8px; padding: 12px; background: #f8f9fa; border-radius: 4px; max-height: 400px; overflow-y: auto;"><%= @session.query %></pre>
13
+ </details>
14
+ </div>
15
+ <% else %>
16
+ <span><%= @session.query %></span>
17
+ <% end %>
8
18
  </div>
9
19
  <% if @session.name.present? %>
10
20
  <div class="meta-item">
@@ -29,12 +39,12 @@
29
39
  <span class="mono"><%= @session.input_tokens %> / <%= @session.output_tokens %></span>
30
40
  </div>
31
41
  <div class="meta-item">
32
- <label>Duration</label>
33
- <span class="mono"><%= @session.duration_ms ? "#{@session.duration_ms}ms" : '-' %></span>
42
+ <label>Est. Cost</label>
43
+ <span class="mono"><%= format_cost(@session) %></span>
34
44
  </div>
35
45
  <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>
46
+ <label>Duration</label>
47
+ <span class="mono"><%= @session.duration_ms ? "#{@session.duration_ms}ms" : '-' %></span>
38
48
  </div>
39
49
  <div class="meta-item">
40
50
  <label>Time</label>
@@ -54,6 +54,7 @@
54
54
  overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
55
55
  }
56
56
  .mono { font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace; font-size: 13px; }
57
+ .query-cell { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
57
58
  .text-muted { color: #888; }
58
59
  .back-link { margin-bottom: 16px; display: inline-block; }
59
60
  .check { color: #28a745; }
@@ -0,0 +1,23 @@
1
+ module ConsoleAgent
2
+ module Channel
3
+ class Base
4
+ def display(text); raise NotImplementedError; end
5
+ def display_dim(text); raise NotImplementedError; end
6
+ def display_warning(text); raise NotImplementedError; end
7
+ def display_error(text); raise NotImplementedError; end
8
+ def display_code(code); raise NotImplementedError; end
9
+ def display_result(text); raise NotImplementedError; end
10
+ def display_result_output(text); end # stdout output from code execution
11
+ def prompt(text); raise NotImplementedError; end
12
+ def confirm(text); raise NotImplementedError; end
13
+ def user_identity; raise NotImplementedError; end
14
+ def mode; raise NotImplementedError; end
15
+ def cancelled?; false; end
16
+ def supports_danger?; true; end
17
+ def supports_editing?; false; end
18
+ def edit_code(code); code; end
19
+ def wrap_llm_call(&block); yield; end
20
+ def system_instructions; nil; end
21
+ end
22
+ end
23
+ end