console_agent 0.0.1 → 0.2.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: 528fd68c058820f080d3b6d2aec7767ac48944e17ae6f2c117d279fbec4c8070
4
- data.tar.gz: cf28f6caa721b229536bb591c1c5ecfbb51b1dbc55cee342bccfdaabbca0e804
3
+ metadata.gz: 53925e32cd4919e550bfdc081ede866613ce3ea18a585aca6cb81d1687ce3901
4
+ data.tar.gz: ae601be1b81ff2fcb89addda1c008c0646118f2af21c9009a1792556ffd9b4ed
5
5
  SHA512:
6
- metadata.gz: 9127d4e6c9eea9cb56cd4484906fd80c97f07d2c276f8d65f5906338011d03c39a53e5c1a5bfc3e1cbd3ecb69a4b6ddd9716a1aa8416b091c056ad5f267f21b2
7
- data.tar.gz: 567dea6f8c8f8213ad3ed4b5cafa9519f3ecbbaa5825355cbee1be8a56981565dd3ec4905285ad7bdae8c6c5ea312550396ce1dfd2fe1c98df3e29d7d43b5a3e
6
+ metadata.gz: 54ec67e116e4a05de7a5d915f93dc7babf2d64d2b008bc507718dce92d58ad032beeaa1d7710d8190943454f937f91780ff5614e44f5ef173ab5feff6dddb339
7
+ data.tar.gz: f12af224368e44b149d740f8d0b2dee4a38ce86564ee9337c5a8ed569a83bb387d3edcf2ae76142d4b510f507c5e6d64d82aab70c16eb94f66d83e3811eb0cd6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frank Cort
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # ConsoleAgent
2
+
3
+ An AI-powered assistant for your Rails console. Ask questions in plain English, get executable Ruby code.
4
+
5
+ It's like Claude Code embedded in your Rails Console.
6
+
7
+ ConsoleAgent connects your `rails console` to an LLM (Claude or GPT) and gives it tools to introspect your app's database schema, models, and source code. It figures out what it needs on its own, so you don't pay for 50K tokens of context on every query.
8
+
9
+ ## Quick Start
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'console_agent', group: :development
15
+ ```
16
+
17
+ Run the install generator:
18
+
19
+ ```bash
20
+ bundle install
21
+ rails generate console_agent:install
22
+ ```
23
+
24
+ Set your API key (pick one):
25
+
26
+ ```bash
27
+ # Option A: environment variable
28
+ export ANTHROPIC_API_KEY=sk-ant-...
29
+
30
+ # Option B: in the initializer
31
+ # config.api_key = 'sk-ant-...'
32
+ ```
33
+
34
+ Open a console and go:
35
+
36
+ ```ruby
37
+ rails console
38
+ ai "show me all users who signed up this week"
39
+ ```
40
+
41
+ You can also set or change your API key at runtime in the console:
42
+
43
+ ```ruby
44
+ ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
45
+ ```
46
+
47
+ ## Console Commands
48
+
49
+ | Command | Description |
50
+ |---------|-------------|
51
+ | `ai "query"` | Ask a question, review generated code, confirm before executing |
52
+ | `ai! "query"` | Ask a question and enter interactive mode for follow-ups |
53
+ | `ai!` | Enter interactive mode (type `exit` to leave) |
54
+ | `ai? "query"` | Explain only — shows code but never executes |
55
+ | `ai_status` | Print current configuration summary |
56
+
57
+ ### Example Session
58
+
59
+ ```
60
+ irb> ai "find the 5 most recent orders over $100"
61
+ Thinking...
62
+ -> list_tables
63
+ 12 tables: users, orders, line_items, products...
64
+ -> describe_table("orders")
65
+ 8 columns
66
+ -> describe_model("Order")
67
+ 4 associations, 2 validations
68
+
69
+ You can query recent high-value orders like this:
70
+
71
+ Order.where("total > ?", 100).order(created_at: :desc).limit(5)
72
+
73
+ [tokens in: 1,240 | out: 85 | total: 1,325]
74
+ Execute? [y/N/edit] y
75
+ => [#<Order id: 4821, ...>, ...]
76
+ ```
77
+
78
+ ### Interactive Mode
79
+
80
+ ```
81
+ irb> ai!
82
+ ConsoleAgent interactive mode. Type 'exit' or 'quit' to leave.
83
+ ai> show me all tables
84
+ Thinking...
85
+ -> list_tables
86
+ 12 tables: users, orders, line_items, products...
87
+ ...
88
+ ai> now count orders by status
89
+ ...
90
+ ai> exit
91
+ [session totals — in: 3,200 | out: 410 | total: 3,610]
92
+ Left ConsoleAgent interactive mode.
93
+ ```
94
+
95
+ ### Configuration Status
96
+
97
+ ```
98
+ irb> ai_status
99
+ [ConsoleAgent v0.1.0]
100
+ Provider: anthropic
101
+ Model: claude-opus-4-6
102
+ API key: sk-ant-...a3b4
103
+ Context mode: smart
104
+ Max tokens: 4096
105
+ Temperature: 0.2
106
+ Timeout: 30s
107
+ Max tool rounds:10
108
+ Auto-execute: false
109
+ Debug: false
110
+ ```
111
+
112
+ ## Configuration
113
+
114
+ The install generator creates `config/initializers/console_agent.rb`:
115
+
116
+ ```ruby
117
+ ConsoleAgent.configure do |config|
118
+ # LLM provider: :anthropic or :openai
119
+ config.provider = :anthropic
120
+
121
+ # API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
122
+ # config.api_key = 'sk-...'
123
+
124
+ # Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
125
+ # config.model = 'claude-opus-4-6'
126
+
127
+ # Context mode:
128
+ # :smart - (default) LLM uses tools to fetch schema/model/code on demand
129
+ # :full - sends all schema, models, and routes every time
130
+ config.context_mode = :smart
131
+
132
+ # Max tokens for LLM response
133
+ config.max_tokens = 4096
134
+
135
+ # Temperature (0.0 - 1.0)
136
+ config.temperature = 0.2
137
+
138
+ # Auto-execute generated code without confirmation
139
+ config.auto_execute = false
140
+
141
+ # Max tool-use rounds per query in :smart mode
142
+ config.max_tool_rounds = 10
143
+
144
+ # HTTP timeout in seconds
145
+ config.timeout = 30
146
+
147
+ # Debug mode: prints full API requests/responses and tool calls
148
+ # config.debug = true
149
+ end
150
+ ```
151
+
152
+ All settings can be changed at runtime in the console:
153
+
154
+ ```ruby
155
+ ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
156
+ ConsoleAgent.configure { |c| c.debug = true }
157
+ ConsoleAgent.configure { |c| c.provider = :openai; c.api_key = 'sk-...' }
158
+ ```
159
+
160
+ ## Context Modes
161
+
162
+ ### Smart Mode (default)
163
+
164
+ The LLM gets a minimal system prompt and uses tools to look up what it needs:
165
+
166
+ - **list_tables** / **describe_table** — database schema on demand
167
+ - **list_models** / **describe_model** — ActiveRecord associations, validations
168
+ - **list_files** / **read_file** / **search_code** — browse app source code
169
+
170
+ This keeps token usage low (~1-3K per query) even for large apps with hundreds of tables.
171
+
172
+ ### Full Mode
173
+
174
+ Sends the entire database schema, all model details, and a route summary in every request. Simple but expensive for large apps (~50K+ tokens). Useful if you want everything available without tool-call round trips.
175
+
176
+ ```ruby
177
+ ConsoleAgent.configure { |c| c.context_mode = :full }
178
+ ```
179
+
180
+ ## Tools Available in Smart Mode
181
+
182
+ | Tool | Description |
183
+ |------|-------------|
184
+ | `list_tables` | All database table names |
185
+ | `describe_table` | Columns, types, and indexes for one table |
186
+ | `list_models` | All model names with association names |
187
+ | `describe_model` | Associations, validations, scopes for one model |
188
+ | `list_files` | Ruby files in a directory |
189
+ | `read_file` | Read a source file (capped at 200 lines) |
190
+ | `search_code` | Grep for a pattern across Ruby files |
191
+ | `ask_user` | Ask the console user a clarifying question |
192
+ | `save_memory` | Persist a learned fact for future sessions |
193
+ | `recall_memories` | Search saved memories |
194
+ | `load_skill` | Load full instructions for a skill |
195
+
196
+ The LLM decides which tools to call based on your question. You can see the tool calls happening in real time.
197
+
198
+ ## Memories & Skills
199
+
200
+ ConsoleAgent can remember what it learns about your codebase across sessions and load reusable skill instructions on demand.
201
+
202
+ ### Memories
203
+
204
+ When the AI investigates something complex (like how sharding works in your app), it can save what it learned for next time. Memories are stored in `.console_agent/memories.yml` in your Rails app.
205
+
206
+ ```
207
+ ai> how many users are on this shard?
208
+ Thinking...
209
+ -> search_code("shard")
210
+ Found 50 matches
211
+ -> read_file("config/initializers/sharding.rb")
212
+ 202 lines
213
+ -> save_memory("Sharding architecture")
214
+ Memory saved: "Sharding architecture"
215
+
216
+ Each shard is a separate database. User.count returns the count for the current shard only.
217
+
218
+ User.count
219
+
220
+ [tokens in: 2,340 | out: 120 | total: 2,460]
221
+ ```
222
+
223
+ Next session, the AI already knows:
224
+
225
+ ```
226
+ ai> count users on this shard
227
+ Thinking...
228
+
229
+ User.count
230
+
231
+ [tokens in: 580 | out: 45 | total: 625]
232
+ ```
233
+
234
+ The memory file is human-editable YAML:
235
+
236
+ ```yaml
237
+ # .console_agent/memories.yml
238
+ memories:
239
+ - id: mem_1709123456_42
240
+ name: Sharding architecture
241
+ description: "App uses database-per-shard. Each shard is a separate DB. User.count only counts the current shard."
242
+ tags: [database, sharding]
243
+ created_at: "2026-02-27T10:30:00Z"
244
+ ```
245
+
246
+ You can commit this file to your repo so the whole team benefits, or gitignore it for personal use.
247
+
248
+ ### Skills
249
+
250
+ Skills are reusable instruction files that teach the AI how to handle specific tasks. Store them in `.console_agent/skills/` as markdown files with YAML frontmatter:
251
+
252
+ ```markdown
253
+ # .console_agent/skills/sharding.md
254
+ ---
255
+ name: Sharded database queries
256
+ description: How to query across database shards. Use when user asks about counting records across shards or shard-specific operations.
257
+ ---
258
+
259
+ ## Instructions
260
+
261
+ This app uses Apartment with one database per shard.
262
+ - `User.count` only counts the current shard
263
+ - To count across all shards: `Shard.all.sum { |s| s.switch { User.count } }`
264
+ - Current shard: `Apartment::Tenant.current`
265
+ ```
266
+
267
+ Only the skill name and description are sent to the AI on every request (~100 tokens per skill). The full instructions are loaded on demand via the `load_skill` tool only when relevant.
268
+
269
+ ### Storage
270
+
271
+ By default, memories and skills are stored on the filesystem at `Rails.root/.console_agent/`. This works for development and for production environments with a persistent filesystem.
272
+
273
+ For containers or other environments where the filesystem is ephemeral, you have two options:
274
+
275
+ **Option A: Commit to your repo.** Create `.console_agent/memories.yml` and `.console_agent/skills/*.md` in your codebase. They'll be baked into your Docker image.
276
+
277
+ **Option B: Custom storage adapter.** Implement the storage interface and plug it in:
278
+
279
+ ```ruby
280
+ ConsoleAgent.configure do |config|
281
+ config.storage_adapter = MyS3Storage.new(bucket: 'my-bucket', prefix: 'console_agent/')
282
+ end
283
+ ```
284
+
285
+ A storage adapter just needs four methods: `read(key)`, `write(key, content)`, `list(pattern)`, and `exists?(key)`.
286
+
287
+ To disable memories or skills:
288
+
289
+ ```ruby
290
+ ConsoleAgent.configure do |config|
291
+ config.memories_enabled = false
292
+ config.skills_enabled = false
293
+ end
294
+ ```
295
+
296
+ ## Providers
297
+
298
+ ### Anthropic (default)
299
+
300
+ Uses the Claude Messages API. Set `ANTHROPIC_API_KEY` or `config.api_key`.
301
+
302
+ ### OpenAI
303
+
304
+ Uses the Chat Completions API. Set `OPENAI_API_KEY` or `config.api_key`.
305
+
306
+ ```ruby
307
+ ConsoleAgent.configure do |config|
308
+ config.provider = :openai
309
+ config.model = 'gpt-5.3-codex' # optional, this is the default
310
+ end
311
+ ```
312
+
313
+ ## Local Development
314
+
315
+ To develop the gem locally against a Rails app, use a path reference in your Gemfile:
316
+
317
+ ```ruby
318
+ gem 'console_agent', path: '/path/to/console_agent'
319
+ ```
320
+
321
+ Switch back to the published gem when you're done:
322
+
323
+ ```ruby
324
+ gem 'console_agent'
325
+ ```
326
+
327
+ ## Requirements
328
+
329
+ - Ruby >= 2.5
330
+ - Rails >= 5.0
331
+ - Faraday >= 1.0
332
+
333
+ ## License
334
+
335
+ MIT
@@ -0,0 +1,61 @@
1
+ module ConsoleAgent
2
+ class Configuration
3
+ PROVIDERS = %i[anthropic openai].freeze
4
+ CONTEXT_MODES = %i[full smart].freeze
5
+
6
+ attr_accessor :provider, :api_key, :model, :max_tokens,
7
+ :auto_execute, :context_mode, :temperature,
8
+ :timeout, :debug, :max_tool_rounds,
9
+ :storage_adapter, :memories_enabled
10
+
11
+ def initialize
12
+ @provider = :anthropic
13
+ @api_key = nil
14
+ @model = nil
15
+ @max_tokens = 4096
16
+ @auto_execute = false
17
+ @context_mode = :smart
18
+ @temperature = 0.2
19
+ @timeout = 30
20
+ @debug = false
21
+ @max_tool_rounds = 100
22
+ @storage_adapter = nil
23
+ @memories_enabled = true
24
+ end
25
+
26
+ def resolved_api_key
27
+ return @api_key if @api_key && !@api_key.empty?
28
+
29
+ case @provider
30
+ when :anthropic
31
+ ENV['ANTHROPIC_API_KEY']
32
+ when :openai
33
+ ENV['OPENAI_API_KEY']
34
+ end
35
+ end
36
+
37
+ def resolved_model
38
+ return @model if @model && !@model.empty?
39
+
40
+ case @provider
41
+ when :anthropic
42
+ 'claude-opus-4-6'
43
+ when :openai
44
+ 'gpt-5.3-codex'
45
+ end
46
+ end
47
+
48
+ def validate!
49
+ unless PROVIDERS.include?(@provider)
50
+ raise ConfigurationError, "Unknown provider: #{@provider}. Valid: #{PROVIDERS.join(', ')}"
51
+ end
52
+
53
+ unless resolved_api_key
54
+ env_var = @provider == :anthropic ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'
55
+ raise ConfigurationError, "No API key. Set config.api_key or #{env_var} env var."
56
+ end
57
+ end
58
+ end
59
+
60
+ class ConfigurationError < StandardError; end
61
+ end
@@ -0,0 +1,131 @@
1
+ module ConsoleAgent
2
+ module ConsoleMethods
3
+ def ai_status
4
+ ConsoleAgent.status
5
+ end
6
+
7
+ def ai_memories(n = nil)
8
+ require 'yaml'
9
+ require 'console_agent/tools/memory_tools'
10
+ storage = ConsoleAgent.storage
11
+ keys = storage.list('memories/*.md').sort
12
+
13
+ if keys.empty?
14
+ $stdout.puts "\e[2mNo memories stored yet.\e[0m"
15
+ return nil
16
+ end
17
+
18
+ memories = keys.filter_map do |key|
19
+ content = storage.read(key)
20
+ next if content.nil? || content.strip.empty?
21
+ next unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
22
+ fm = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
23
+ fm.merge('description' => $2.strip, 'file' => key)
24
+ end
25
+
26
+ if memories.empty?
27
+ $stdout.puts "\e[2mNo memories stored yet.\e[0m"
28
+ return nil
29
+ end
30
+
31
+ shown = n ? memories.last(n) : memories.last(5)
32
+ total = memories.length
33
+
34
+ $stdout.puts "\e[36m[Memories — showing last #{shown.length} of #{total}]\e[0m"
35
+ shown.each do |m|
36
+ $stdout.puts "\e[33m #{m['name']}\e[0m"
37
+ $stdout.puts "\e[2m #{m['description']}\e[0m"
38
+ tags = Array(m['tags'])
39
+ $stdout.puts "\e[2m tags: #{tags.join(', ')}\e[0m" unless tags.empty?
40
+ $stdout.puts
41
+ end
42
+
43
+ path = storage.respond_to?(:root_path) ? File.join(storage.root_path, 'memories') : 'memories/'
44
+ $stdout.puts "\e[2mStored in: #{path}/\e[0m"
45
+ $stdout.puts "\e[2mUse ai_memories(n) to show last n.\e[0m"
46
+ nil
47
+ end
48
+
49
+ def ai(query = nil)
50
+ if query.nil?
51
+ $stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
52
+ $stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
53
+ $stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
54
+ $stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
55
+ $stderr.puts "\e[33m ai_status - show current configuration\e[0m"
56
+ $stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
57
+ return nil
58
+ end
59
+
60
+ require 'console_agent/context_builder'
61
+ require 'console_agent/providers/base'
62
+ require 'console_agent/executor'
63
+ require 'console_agent/repl'
64
+
65
+ repl = Repl.new(__console_agent_binding)
66
+ repl.one_shot(query.to_s)
67
+ rescue => e
68
+ $stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
69
+ nil
70
+ end
71
+
72
+ def ai!(query = nil)
73
+ require 'console_agent/context_builder'
74
+ require 'console_agent/providers/base'
75
+ require 'console_agent/executor'
76
+ require 'console_agent/repl'
77
+
78
+ repl = Repl.new(__console_agent_binding)
79
+
80
+ if query
81
+ repl.one_shot(query.to_s)
82
+ else
83
+ repl.interactive
84
+ end
85
+ rescue => e
86
+ $stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
87
+ nil
88
+ end
89
+
90
+ def ai?(query = nil)
91
+ unless query
92
+ $stderr.puts "\e[33mUsage: ai? \"your question here\" - explain without executing\e[0m"
93
+ return nil
94
+ end
95
+
96
+ require 'console_agent/context_builder'
97
+ require 'console_agent/providers/base'
98
+ require 'console_agent/executor'
99
+ require 'console_agent/repl'
100
+
101
+ repl = Repl.new(__console_agent_binding)
102
+ repl.explain(query.to_s)
103
+ rescue => e
104
+ $stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
105
+ nil
106
+ end
107
+
108
+ private
109
+
110
+ def __console_agent_binding
111
+ # Try IRB workspace binding
112
+ if defined?(IRB) && IRB.respond_to?(:CurrentContext)
113
+ ctx = IRB.CurrentContext rescue nil
114
+ if ctx && ctx.respond_to?(:workspace) && ctx.workspace.respond_to?(:binding)
115
+ return ctx.workspace.binding
116
+ end
117
+ end
118
+
119
+ # Try Pry binding
120
+ if defined?(Pry) && respond_to?(:pry_instance, true)
121
+ pry_inst = pry_instance rescue nil
122
+ if pry_inst && pry_inst.respond_to?(:current_binding)
123
+ return pry_inst.current_binding
124
+ end
125
+ end
126
+
127
+ # Fallback
128
+ TOPLEVEL_BINDING
129
+ end
130
+ end
131
+ end