console_agent 0.4.0 → 0.6.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: 28a64e6be4f7d39d73e55b67d6e95404324355420293651acf44fd439394eb56
4
- data.tar.gz: 8350d0a49d94733c54530debc4d8e184bd0c2774b04f82c68be24b71d32a7457
3
+ metadata.gz: d1d37fc84ac3f2d29832d282e6db1fd394308de2b967843c3a0c9748560a7025
4
+ data.tar.gz: 28bd6c25529dbedfa12d2353a1648304517bab933f0a4f25e91285ccaaf08e10
5
5
  SHA512:
6
- metadata.gz: 874af072ffd5222c5ef7c782e1b5db18a148ecaf013a6ba8f2e33d863123895235c1d93106551ad5197eb35c17ad2bf65b076a79e74ef091ac76690c59fe4ba8
7
- data.tar.gz: c2ba1ece779bf4e12b30c9bc0ed533dc9acabdaa22d6749e8f3f75d0d18fea1cb3895c669c313ec005f1e79a3a72ab7e2d16a2fefdcbc7df8509eac35afaee92
6
+ metadata.gz: 6451c8b62eba1159e0233be04dabbc86bcb3b6f1dcbd6cd658c8d5517a27f1ad6a20884d30d87fabcbe75d8a51d12a6dfa2d4a59ed0adb648ce56d3b81765636
7
+ data.tar.gz: 3448b25930e4613a4599698d98ec1c5a2d84b113a3221d98239dc0f09351455be817882b308910bfd8bf01eab00d01670c11256624bb7cd7a30745538095c2ea
data/README.md CHANGED
@@ -59,6 +59,7 @@ The AI calls tools behind the scenes to learn your app — schema, models, assoc
59
59
  | `ai "query"` | One-shot: ask, review code, confirm |
60
60
  | `ai! "query"` | Interactive: ask and keep chatting |
61
61
  | `ai? "query"` | Explain only, never executes |
62
+ | `ai_init` | Generate/update app guide for better context |
62
63
 
63
64
  ### Multi-Step Plans
64
65
 
@@ -112,12 +113,36 @@ ai> how does sharding work?
112
113
 
113
114
  Next time, it already knows — no re-reading files, fewer tokens.
114
115
 
116
+ ### Application Guide
117
+
118
+ Run `ai_init` to have the AI explore your app and generate a guide that gets loaded into every future conversation:
119
+
120
+ ```
121
+ irb> ai_init
122
+ No existing guide. Exploring the app...
123
+ Thinking...
124
+ -> list_models
125
+ 240 models
126
+ -> describe_model("User")
127
+ 119 associations, 6 validations
128
+ -> describe_model("Account")
129
+ 25 associations
130
+ -> search_code("Sharding", dir: "config")
131
+ Found 36 matches
132
+ ...
133
+ Guide saved to .console_agent/console_agent.md (3204 chars)
134
+ ```
135
+
136
+ The guide is a markdown file covering your app's models, relationships, data architecture, and gotchas. Unlike memories (which require a tool call to recall), the guide is injected directly into the system prompt — so the AI starts every session already knowing your app.
137
+
138
+ Run `ai_init` again anytime to update it.
139
+
115
140
  ### Interactive Mode
116
141
 
117
142
  ```
118
143
  irb> ai!
119
144
  ConsoleAgent interactive mode. Type 'exit' to leave.
120
- Auto-execute: OFF (Shift-Tab or /auto to toggle)
145
+ Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
121
146
 
122
147
  ai> show me all tables
123
148
  ...
@@ -132,6 +157,21 @@ ai> exit
132
157
 
133
158
  Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
134
159
 
160
+ ### Direct Code Execution
161
+
162
+ Prefix any input with `>` to run Ruby code directly — no LLM round-trip. The result is added to the conversation context, so the AI knows what happened:
163
+
164
+ ```
165
+ ai> >User.count
166
+ => 8
167
+ ai> how many users do I have?
168
+ Thinking...
169
+
170
+ You have **8 users** in your database, as confirmed by the `User.count` you just ran.
171
+ ```
172
+
173
+ Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
174
+
135
175
  ### Sessions
136
176
 
137
177
  Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
@@ -130,12 +130,26 @@ module ConsoleAgent
130
130
  nil
131
131
  end
132
132
 
133
+ def ai_init
134
+ require 'console_agent/context_builder'
135
+ require 'console_agent/providers/base'
136
+ require 'console_agent/executor'
137
+ require 'console_agent/repl'
138
+
139
+ repl = Repl.new(__console_agent_binding)
140
+ repl.init_guide
141
+ rescue => e
142
+ $stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
143
+ nil
144
+ end
145
+
133
146
  def ai(query = nil)
134
147
  if query.nil?
135
148
  $stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
136
149
  $stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
137
150
  $stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
138
151
  $stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
152
+ $stderr.puts "\e[33m ai_init - generate/update app guide for better AI context\e[0m"
139
153
  $stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
140
154
  $stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
141
155
  $stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
@@ -15,10 +15,32 @@ module ConsoleAgent
15
15
  parts = []
16
16
  parts << smart_system_instructions
17
17
  parts << environment_context
18
+ parts << guide_context
18
19
  parts << memory_context
19
20
  parts.compact.join("\n\n")
20
21
  end
21
22
 
23
+ def environment_context
24
+ lines = ["## Environment"]
25
+ lines << "- Ruby #{RUBY_VERSION}"
26
+ lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
27
+
28
+ if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
29
+ adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
30
+ lines << "- Database adapter: #{adapter}"
31
+ end
32
+
33
+ if defined?(Bundler)
34
+ key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
35
+ paperclip carrierwave activestorage shrine
36
+ pg mysql2 sqlite3 mongoid]
37
+ loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
38
+ lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
39
+ end
40
+
41
+ lines.join("\n")
42
+ end
43
+
22
44
  private
23
45
 
24
46
  def smart_system_instructions
@@ -67,25 +89,14 @@ module ConsoleAgent
67
89
  PROMPT
68
90
  end
69
91
 
70
- def environment_context
71
- lines = ["## Environment"]
72
- lines << "- Ruby #{RUBY_VERSION}"
73
- lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
74
-
75
- if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
76
- adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
77
- lines << "- Database adapter: #{adapter}"
78
- end
79
-
80
- if defined?(Bundler)
81
- key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
82
- paperclip carrierwave activestorage shrine
83
- pg mysql2 sqlite3 mongoid]
84
- loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
85
- lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
86
- end
92
+ def guide_context
93
+ content = ConsoleAgent.storage.read(ConsoleAgent::GUIDE_KEY)
94
+ return nil if content.nil? || content.strip.empty?
87
95
 
88
- lines.join("\n")
96
+ "## Application Guide\n\n#{content.strip}"
97
+ rescue => e
98
+ ConsoleAgent.logger.debug("ConsoleAgent: guide context failed: #{e.message}")
99
+ nil
89
100
  end
90
101
 
91
102
  def memory_context
@@ -15,7 +15,7 @@ module ConsoleAgent
15
15
 
16
16
  # Welcome message
17
17
  if $stdout.respond_to?(:tty?) && $stdout.tty?
18
- $stdout.puts "\e[36m[ConsoleAgent] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
18
+ $stdout.puts "\e[36m[ConsoleAgent v#{ConsoleAgent::VERSION}] AI assistant loaded. Try: ai \"show me all tables\"\e[0m"
19
19
  end
20
20
 
21
21
  # Pre-build context in background
@@ -90,6 +90,62 @@ module ConsoleAgent
90
90
  nil
91
91
  end
92
92
 
93
+ def init_guide
94
+ storage = ConsoleAgent.storage
95
+ existing_guide = begin
96
+ content = storage.read(ConsoleAgent::GUIDE_KEY)
97
+ (content && !content.strip.empty?) ? content.strip : nil
98
+ rescue
99
+ nil
100
+ end
101
+
102
+ if existing_guide
103
+ $stdout.puts "\e[36m Existing guide found (#{existing_guide.length} chars). Will update.\e[0m"
104
+ else
105
+ $stdout.puts "\e[36m No existing guide. Exploring the app...\e[0m"
106
+ end
107
+
108
+ require 'console_agent/tools/registry'
109
+ init_tools = Tools::Registry.new(mode: :init)
110
+ sys_prompt = init_system_prompt(existing_guide)
111
+ messages = [{ role: :user, content: "Explore this Rails application and generate the application guide." }]
112
+
113
+ # Temporarily increase timeout — init conversations are large
114
+ original_timeout = ConsoleAgent.configuration.timeout
115
+ ConsoleAgent.configuration.timeout = [original_timeout, 120].max
116
+
117
+ result, _ = send_query_with_tools(messages, system_prompt: sys_prompt, tools_override: init_tools)
118
+
119
+ guide_text = result.text.to_s.strip
120
+ # Strip markdown code fences if the LLM wrapped the response
121
+ guide_text = guide_text.sub(/\A```(?:markdown)?\s*\n?/, '').sub(/\n?```\s*\z/, '')
122
+ # Strip LLM preamble/thinking before the actual guide content
123
+ guide_text = guide_text.sub(/\A.*?(?=^#\s)/m, '') if guide_text =~ /^#\s/m
124
+
125
+ if guide_text.empty?
126
+ $stdout.puts "\e[33m No guide content generated.\e[0m"
127
+ return nil
128
+ end
129
+
130
+ storage.write(ConsoleAgent::GUIDE_KEY, guide_text)
131
+
132
+ path = storage.respond_to?(:root_path) ? File.join(storage.root_path, ConsoleAgent::GUIDE_KEY) : ConsoleAgent::GUIDE_KEY
133
+ $stdout.puts "\e[32m Guide saved to #{path} (#{guide_text.length} chars)\e[0m"
134
+ display_usage(result)
135
+ nil
136
+ rescue Interrupt
137
+ $stdout.puts "\n\e[33m Interrupted.\e[0m"
138
+ nil
139
+ rescue Providers::ProviderError => e
140
+ $stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
141
+ nil
142
+ rescue => e
143
+ $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
144
+ nil
145
+ ensure
146
+ ConsoleAgent.configuration.timeout = original_timeout if original_timeout
147
+ end
148
+
93
149
  def interactive
94
150
  init_interactive_state
95
151
  interactive_loop
@@ -146,7 +202,7 @@ module ConsoleAgent
146
202
  name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
147
203
  # Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
148
204
  @interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
149
- @interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | /usage | /name <label>\e[0m"
205
+ @interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>\e[0m"
150
206
 
151
207
  # Bind Shift-Tab to insert /auto command and submit
152
208
  if Readline.respond_to?(:parse_and_bind)
@@ -199,6 +255,35 @@ module ConsoleAgent
199
255
  next
200
256
  end
201
257
 
258
+ # Direct code execution with ">" prefix — skip LLM entirely
259
+ if input.start_with?('>') && !input.start_with?('>=')
260
+ raw_code = input.sub(/\A>\s?/, '')
261
+ Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
262
+ @interactive_console_capture.write("ai> #{input}\n")
263
+
264
+ exec_result = @executor.execute(raw_code)
265
+
266
+ output_parts = []
267
+ output_parts << "Output:\n#{@executor.last_output.strip}" if @executor.last_output && !@executor.last_output.strip.empty?
268
+ output_parts << "Return value: #{exec_result.inspect}" if exec_result
269
+
270
+ result_str = output_parts.join("\n\n")
271
+ result_str = result_str[0..1000] + '...' if result_str.length > 1000
272
+
273
+ context_msg = "User directly executed code: `#{raw_code}`"
274
+ context_msg += "\n#{result_str}" unless output_parts.empty?
275
+ @history << { role: :user, content: context_msg }
276
+
277
+ @interactive_query ||= input
278
+ @last_interactive_code = raw_code
279
+ @last_interactive_output = @executor.last_output
280
+ @last_interactive_result = exec_result ? exec_result.inspect : nil
281
+ @last_interactive_executed = true
282
+
283
+ log_interactive_turn
284
+ next
285
+ end
286
+
202
287
  # Add to Readline history (avoid consecutive duplicates)
203
288
  Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
204
289
 
@@ -301,6 +386,50 @@ module ConsoleAgent
301
386
  @context ||= context_builder.build
302
387
  end
303
388
 
389
+ def init_system_prompt(existing_guide)
390
+ env = context_builder.environment_context
391
+
392
+ prompt = <<~PROMPT
393
+ You are a Rails application analyst. Your job is to explore this Rails app using the
394
+ available tools and produce a concise markdown guide that will be injected into future
395
+ AI assistant sessions.
396
+
397
+ #{env}
398
+
399
+ EXPLORATION STRATEGY — be efficient to avoid timeouts:
400
+ 1. Start with list_models to see all models and their associations
401
+ 2. Pick the 5-8 CORE models and call describe_model on those only
402
+ 3. Call describe_table on only 3-5 key tables (skip tables whose models already told you enough)
403
+ 4. Use search_code sparingly — only for specific patterns you suspect (sharding, STI, concerns)
404
+ 5. Use read_file only when you need to understand a specific pattern (read small sections, not whole files)
405
+ 6. Do NOT exhaustively describe every table or model — focus on what's important
406
+
407
+ IMPORTANT: Keep your total tool calls under 20. Prioritize breadth over depth.
408
+
409
+ Produce a markdown document with these sections:
410
+ - **Application Overview**: What the app does, key domain concepts
411
+ - **Key Models & Relationships**: Core models and how they relate
412
+ - **Data Architecture**: Important tables, notable columns, any partitioning/sharding
413
+ - **Important Patterns**: Custom concerns, service objects, key abstractions
414
+ - **Common Maintenance Tasks**: Typical console operations for this app
415
+ - **Gotchas**: Non-obvious behaviors, tricky associations, known quirks
416
+
417
+ Keep it concise — aim for 1-2 pages. Focus on what a console user needs to know.
418
+ Do NOT wrap the output in markdown code fences.
419
+ PROMPT
420
+
421
+ if existing_guide
422
+ prompt += <<~UPDATE
423
+
424
+ Here is the existing guide. Update and merge with any new findings:
425
+
426
+ #{existing_guide}
427
+ UPDATE
428
+ end
429
+
430
+ prompt.strip
431
+ end
432
+
304
433
  def send_query(query, conversation: nil)
305
434
  ConsoleAgent.configuration.validate!
306
435
 
@@ -313,45 +442,43 @@ module ConsoleAgent
313
442
  send_query_with_tools(messages)
314
443
  end
315
444
 
316
- def send_query_with_tools(messages)
445
+ def send_query_with_tools(messages, system_prompt: nil, tools_override: nil)
317
446
  require 'console_agent/tools/registry'
318
- tools = Tools::Registry.new(executor: @executor)
447
+ tools = tools_override || Tools::Registry.new(executor: @executor)
448
+ active_system_prompt = system_prompt || context
319
449
  max_rounds = ConsoleAgent.configuration.max_tool_rounds
320
450
  total_input = 0
321
451
  total_output = 0
322
452
  result = nil
323
453
  new_messages = [] # Track messages added during tool use
454
+ last_thinking = nil
455
+ last_tool_names = []
324
456
 
325
457
  exhausted = false
326
458
 
327
459
  max_rounds.times do |round|
328
460
  if round == 0
329
461
  $stdout.puts "\e[2m Thinking...\e[0m"
462
+ else
463
+ # Show buffered thinking text before the "Calling LLM" line
464
+ if last_thinking
465
+ last_thinking.split("\n").each do |line|
466
+ $stdout.puts "\e[2m #{line}\e[0m"
467
+ end
468
+ end
469
+ $stdout.puts "\e[2m #{llm_status(round, messages, total_input, last_thinking, last_tool_names)}\e[0m"
330
470
  end
331
471
 
332
- begin
333
- result = with_escape_monitoring do
334
- provider.chat_with_tools(messages, tools: tools, system_prompt: context)
335
- end
336
- rescue Interrupt
337
- redirect = prompt_for_redirect
338
- if redirect
339
- messages << { role: :user, content: redirect }
340
- new_messages << messages.last
341
- next
342
- else
343
- raise
344
- end
472
+ result = with_escape_monitoring do
473
+ provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
345
474
  end
346
475
  total_input += result.input_tokens || 0
347
476
  total_output += result.output_tokens || 0
348
477
 
349
478
  break unless result.tool_use?
350
479
 
351
- # Show what the LLM is thinking (if it returned text alongside tool calls)
352
- if result.text && !result.text.strip.empty?
353
- $stdout.puts "\e[2m #{result.text.strip}\e[0m"
354
- end
480
+ # Buffer thinking text for display before next LLM call
481
+ last_thinking = (result.text && !result.text.strip.empty?) ? result.text.strip : nil
355
482
 
356
483
  # Add assistant message with tool calls to conversation
357
484
  assistant_msg = provider.format_assistant_message(result)
@@ -359,6 +486,7 @@ module ConsoleAgent
359
486
  new_messages << assistant_msg
360
487
 
361
488
  # Execute each tool and show progress
489
+ last_tool_names = result.tool_calls.map { |tc| tc[:name] }
362
490
  result.tool_calls.each do |tc|
363
491
  # ask_user and execute_plan handle their own display
364
492
  if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
@@ -390,7 +518,7 @@ module ConsoleAgent
390
518
  if exhausted
391
519
  $stdout.puts "\e[33m Hit tool round limit (#{max_rounds}). Forcing final answer. Increase with: ConsoleAgent.configure { |c| c.max_tool_rounds = 200 }\e[0m"
392
520
  messages << { role: :user, content: "You've used all available tool rounds. Please provide your best answer now based on what you've learned so far." }
393
- result = provider.chat(messages, system_prompt: context)
521
+ result = provider.chat(messages, system_prompt: active_system_prompt)
394
522
  total_input += result.input_tokens || 0
395
523
  total_output += result.output_tokens || 0
396
524
  end
@@ -448,13 +576,29 @@ module ConsoleAgent
448
576
  end
449
577
  end
450
578
 
451
- def prompt_for_redirect
452
- $stdout.puts "\n\e[33m Interrupted. What should the AI do differently?\e[0m"
453
- $stdout.puts "\e[2m (Press Enter with no input to abort entirely)\e[0m"
454
- $stdout.print "\e[33m redirect> \e[0m"
455
- input = $stdin.gets
456
- return nil if input.nil? || input.strip.empty?
457
- input.strip
579
+
580
+ def llm_status(round, messages, tokens_so_far, last_thinking = nil, last_tool_names = [])
581
+ status = "Calling LLM (round #{round + 1}, #{messages.length} msgs"
582
+ status += ", ~#{format_tokens(tokens_so_far)} ctx" if tokens_so_far > 0
583
+ status += ")"
584
+ if !last_thinking && last_tool_names.any?
585
+ # Summarize tools when there's no thinking text
586
+ counts = last_tool_names.tally
587
+ summary = counts.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")
588
+ status += " after #{summary}"
589
+ end
590
+ status += "..."
591
+ status
592
+ end
593
+
594
+ def format_tokens(count)
595
+ if count >= 1_000_000
596
+ "#{(count / 1_000_000.0).round(1)}M"
597
+ elsif count >= 1_000
598
+ "#{(count / 1_000.0).round(1)}K"
599
+ else
600
+ count.to_s
601
+ end
458
602
  end
459
603
 
460
604
  def format_tool_args(name, args)
@@ -518,8 +662,12 @@ module ConsoleAgent
518
662
  lines = result.split("\n")
519
663
  "#{lines.length} files"
520
664
  when 'read_file'
521
- lines = result.split("\n")
522
- "#{lines.length} lines"
665
+ if result =~ /^Lines (\d+)-(\d+) of (\d+):/
666
+ "lines #{$1}-#{$2} of #{$3}"
667
+ else
668
+ lines = result.split("\n")
669
+ "#{lines.length} lines"
670
+ end
523
671
  when 'search_code'
524
672
  if result.start_with?('Found')
525
673
  result.split("\n").first
@@ -1,7 +1,7 @@
1
1
  module ConsoleAgent
2
2
  module Tools
3
3
  class CodeTools
4
- MAX_FILE_LINES = 200
4
+ MAX_FILE_LINES = 500
5
5
  MAX_LIST_ENTRIES = 100
6
6
  MAX_SEARCH_RESULTS = 50
7
7
 
@@ -28,7 +28,7 @@ module ConsoleAgent
28
28
  "Error listing files: #{e.message}"
29
29
  end
30
30
 
31
- def read_file(path)
31
+ def read_file(path, start_line: nil, end_line: nil)
32
32
  return "Error: path is required." if path.nil? || path.strip.empty?
33
33
 
34
34
  root = rails_root
@@ -45,12 +45,24 @@ module ConsoleAgent
45
45
  return "File '#{path}' not found." unless File.exist?(full_path)
46
46
  return "Error: '#{path}' is a directory, not a file." if File.directory?(full_path)
47
47
 
48
- lines = File.readlines(full_path)
49
- if lines.length > MAX_FILE_LINES
50
- numbered = lines.first(MAX_FILE_LINES).each_with_index.map { |line, i| "#{i + 1}: #{line}" }
51
- numbered.join + "\n... truncated (#{lines.length} total lines, showing first #{MAX_FILE_LINES})"
48
+ all_lines = File.readlines(full_path)
49
+ total = all_lines.length
50
+
51
+ # Apply line range if specified (1-based, inclusive)
52
+ if start_line || end_line
53
+ s = [(start_line || 1).to_i, 1].max
54
+ e = [(end_line || total).to_i, total].min
55
+ return "Error: start_line (#{s}) is beyond end of file (#{total} lines)." if s > total
56
+ lines = all_lines[(s - 1)..(e - 1)] || []
57
+ offset = s - 1
58
+ numbered = lines.each_with_index.map { |line, i| "#{offset + i + 1}: #{line}" }
59
+ header = "Lines #{s}-#{[e, s + lines.length - 1].min} of #{total}:\n"
60
+ header + numbered.join
61
+ elsif total > MAX_FILE_LINES
62
+ numbered = all_lines.first(MAX_FILE_LINES).each_with_index.map { |line, i| "#{i + 1}: #{line}" }
63
+ numbered.join + "\n... truncated (#{total} total lines, showing first #{MAX_FILE_LINES}). Use start_line/end_line to read specific sections."
52
64
  else
53
- lines.each_with_index.map { |line, i| "#{i + 1}: #{line}" }.join
65
+ all_lines.each_with_index.map { |line, i| "#{i + 1}: #{line}" }.join
54
66
  end
55
67
  rescue => e
56
68
  "Error reading file '#{path}': #{e.message}"
@@ -8,8 +8,9 @@ module ConsoleAgent
8
8
  # Tools that should never be cached (side effects or user interaction)
9
9
  NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
10
10
 
11
- def initialize(executor: nil)
11
+ def initialize(executor: nil, mode: :default)
12
12
  @executor = executor
13
+ @mode = mode
13
14
  @definitions = []
14
15
  @handlers = {}
15
16
  @cache = {}
@@ -142,15 +143,17 @@ module ConsoleAgent
142
143
 
143
144
  register(
144
145
  name: 'read_file',
145
- description: 'Read the contents of a file in this Rails app. Capped at 200 lines.',
146
+ description: 'Read the contents of a file in this Rails app. Returns up to 500 lines by default. Use start_line/end_line to read specific sections of large files.',
146
147
  parameters: {
147
148
  'type' => 'object',
148
149
  'properties' => {
149
- 'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' }
150
+ 'path' => { 'type' => 'string', 'description' => 'Relative file path (e.g. "app/models/user.rb")' },
151
+ 'start_line' => { 'type' => 'integer', 'description' => 'First line to read (1-based). Optional — omit to start from beginning.' },
152
+ 'end_line' => { 'type' => 'integer', 'description' => 'Last line to read (1-based, inclusive). Optional — omit to read to end.' }
150
153
  },
151
154
  'required' => ['path']
152
155
  },
153
- handler: ->(args) { code.read_file(args['path']) }
156
+ handler: ->(args) { code.read_file(args['path'], start_line: args['start_line'], end_line: args['end_line']) }
154
157
  )
155
158
 
156
159
  register(
@@ -167,21 +170,23 @@ module ConsoleAgent
167
170
  handler: ->(args) { code.search_code(args['query'], args['directory']) }
168
171
  )
169
172
 
170
- register(
171
- name: 'ask_user',
172
- description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
173
- parameters: {
174
- 'type' => 'object',
175
- 'properties' => {
176
- 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
173
+ unless @mode == :init
174
+ register(
175
+ name: 'ask_user',
176
+ description: 'Ask the console user a clarifying question. Use this when you need specific information to write accurate code (e.g. which user they are, which record to target, what value to use). Do NOT generate placeholder values like YOUR_USER_ID — ask instead.',
177
+ parameters: {
178
+ 'type' => 'object',
179
+ 'properties' => {
180
+ 'question' => { 'type' => 'string', 'description' => 'The question to ask the user' }
181
+ },
182
+ 'required' => ['question']
177
183
  },
178
- 'required' => ['question']
179
- },
180
- handler: ->(args) { ask_user(args['question']) }
181
- )
184
+ handler: ->(args) { ask_user(args['question']) }
185
+ )
182
186
 
183
- register_memory_tools
184
- register_execute_plan
187
+ register_memory_tools
188
+ register_execute_plan
189
+ end
185
190
  end
186
191
 
187
192
  def register_memory_tools
@@ -285,7 +290,7 @@ module ConsoleAgent
285
290
  when 'a', 'auto'
286
291
  skip_confirmations = true
287
292
  when 'y', 'yes'
288
- # proceed with per-step confirmation
293
+ skip_confirmations = true if steps.length == 1
289
294
  else
290
295
  $stdout.puts "\e[33m Plan declined.\e[0m"
291
296
  feedback = ask_feedback("What would you like changed?")
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '0.6.0'.freeze
3
3
  end
data/lib/console_agent.rb CHANGED
@@ -2,6 +2,8 @@ require 'console_agent/version'
2
2
  require 'console_agent/configuration'
3
3
 
4
4
  module ConsoleAgent
5
+ GUIDE_KEY = 'console_agent.md'.freeze
6
+
5
7
  class << self
6
8
  def configuration
7
9
  @configuration ||= Configuration.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr