console_agent 0.3.0 → 0.5.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: 661ab1a997f36d2b10ea9649aa41367aca039c859c3e2afa01c3cbf8b8dd9345
4
- data.tar.gz: 5cfe513d78a1785b7199fb0ebbbc70e825092e49e4051caf390e3ccb7ccf23c4
3
+ metadata.gz: 35e4cf8ecceb5241ce1b18f2cac6d8a235b3185f7839b2143eca6233390375d5
4
+ data.tar.gz: 7ebfc01003d9995ce65645a1f15955747cd59197c891e294d2152f3101fd583c
5
5
  SHA512:
6
- metadata.gz: 0d7fd63a81886c7abbf4f82db677b6b4bb9151760c378901306e789b3619d7ca31b743f7f4a14a9a28335e775877243b0c4ff8b8bd6cc6acee0c8a02c270eccc
7
- data.tar.gz: 3e0308bd89e0421a4a29dbe8c7b7be6afd584a504559a456f830e1c9a268517d131d6dcf00417347f5f3ea80a434bf9211e0515771fc3e43686f0117bf401f9b
6
+ metadata.gz: 1bfdf0d1b5a6278101d7cda640ac710a1aef589e5a1adc4194876404d897a53bcfa27cd423029abc76a117eb6ab2ba13a14e605c4447c1452ab78d0f7b98b960
7
+ data.tar.gz: 7589590b8dcbfd4268070ce59da381741ca1643152b2689829d741a6cc325e7d9130576629a3155ba0267fb0ea86623a584f9a6e6c2956143e043da43e551501
data/README.md CHANGED
@@ -117,7 +117,7 @@ Next time, it already knows — no re-reading files, fewer tokens.
117
117
  ```
118
118
  irb> ai!
119
119
  ConsoleAgent interactive mode. Type 'exit' to leave.
120
- Auto-execute: OFF (Shift-Tab or /auto to toggle)
120
+ Auto-execute: OFF (Shift-Tab or /auto to toggle) | > code to run directly | /usage | /name <label>
121
121
 
122
122
  ai> show me all tables
123
123
  ...
@@ -132,6 +132,21 @@ ai> exit
132
132
 
133
133
  Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
134
134
 
135
+ ### Direct Code Execution
136
+
137
+ 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:
138
+
139
+ ```
140
+ ai> >User.count
141
+ => 8
142
+ ai> how many users do I have?
143
+ Thinking...
144
+
145
+ You have **8 users** in your database, as confirmed by the `User.count` you just ran.
146
+ ```
147
+
148
+ Useful for quick checks, setting up variables, or giving the AI concrete data to work with.
149
+
135
150
  ### Sessions
136
151
 
137
152
  Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
@@ -84,7 +84,7 @@ module ConsoleAgent
84
84
  nil
85
85
  end
86
86
 
87
- def ai_resume(identifier)
87
+ def ai_resume(identifier = nil)
88
88
  __ensure_console_agent_user
89
89
 
90
90
  require 'console_agent/context_builder'
@@ -93,9 +93,16 @@ module ConsoleAgent
93
93
  require 'console_agent/repl'
94
94
  require 'console_agent/session_logger'
95
95
 
96
- session = __find_session(identifier)
96
+ session = if identifier
97
+ __find_session(identifier)
98
+ else
99
+ session_class = Object.const_get('ConsoleAgent::Session')
100
+ session_class.where(mode: 'interactive', user_name: ConsoleAgent.current_user).recent.first
101
+ end
102
+
97
103
  unless session
98
- $stderr.puts "\e[31mSession not found: #{identifier}\e[0m"
104
+ msg = identifier ? "Session not found: #{identifier}" : "No interactive sessions found."
105
+ $stderr.puts "\e[31m#{msg}\e[0m"
99
106
  return nil
100
107
  end
101
108
 
@@ -106,17 +106,17 @@ module ConsoleAgent
106
106
  @total_input_tokens = session.input_tokens || 0
107
107
  @total_output_tokens = session.output_tokens || 0
108
108
 
109
- # Replay stored console output so the user sees previous context
109
+ # Seed the capture buffer with previous output so it's preserved on save
110
+ @interactive_console_capture.write(session.console_output.to_s)
111
+
112
+ # Replay to the user via the real stdout (bypass TeeIO to avoid double-capture)
110
113
  if session.console_output && !session.console_output.strip.empty?
111
- $stdout.puts "\e[2m--- Replaying previous session output ---\e[0m"
112
- $stdout.puts session.console_output
113
- $stdout.puts "\e[2m--- End of previous output ---\e[0m"
114
- $stdout.puts
114
+ @interactive_old_stdout.puts "\e[2m--- Replaying previous session output ---\e[0m"
115
+ @interactive_old_stdout.puts session.console_output
116
+ @interactive_old_stdout.puts "\e[2m--- End of previous output ---\e[0m"
117
+ @interactive_old_stdout.puts
115
118
  end
116
119
 
117
- # Copy replayed output into the capture buffer so it's preserved on save
118
- @interactive_console_capture.write(session.console_output.to_s)
119
-
120
120
  interactive_loop
121
121
  end
122
122
 
@@ -144,8 +144,9 @@ module ConsoleAgent
144
144
  def interactive_loop
145
145
  auto = ConsoleAgent.configuration.auto_execute
146
146
  name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
147
- $stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
148
- $stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | /usage | /name <label>\e[0m"
147
+ # Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
148
+ @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) | > code to run directly | /usage | /name <label>\e[0m"
149
150
 
150
151
  # Bind Shift-Tab to insert /auto command and submit
151
152
  if Readline.respond_to?(:parse_and_bind)
@@ -153,7 +154,7 @@ module ConsoleAgent
153
154
  end
154
155
 
155
156
  loop do
156
- input = Readline.readline("\e[33mai> \e[0m", false)
157
+ input = Readline.readline("\001\e[33m\002ai> \001\e[0m\002", false)
157
158
  break if input.nil? # Ctrl-D
158
159
 
159
160
  input = input.strip
@@ -163,7 +164,7 @@ module ConsoleAgent
163
164
  if input == '/auto'
164
165
  ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
165
166
  mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
166
- $stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
167
+ @interactive_old_stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
167
168
  next
168
169
  end
169
170
 
@@ -175,7 +176,7 @@ module ConsoleAgent
175
176
  if input == '/debug'
176
177
  ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
177
178
  mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
178
- $stdout.puts "\e[36m Debug: #{mode}\e[0m"
179
+ @interactive_old_stdout.puts "\e[36m Debug: #{mode}\e[0m"
179
180
  next
180
181
  end
181
182
 
@@ -183,9 +184,9 @@ module ConsoleAgent
183
184
  name = input.sub('/name', '').strip
184
185
  if name.empty?
185
186
  if @interactive_session_name
186
- $stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
187
+ @interactive_old_stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
187
188
  else
188
- $stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
189
+ @interactive_old_stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
189
190
  end
190
191
  else
191
192
  @interactive_session_name = name
@@ -193,11 +194,40 @@ module ConsoleAgent
193
194
  require 'console_agent/session_logger'
194
195
  SessionLogger.update(@interactive_session_id, name: name)
195
196
  end
196
- $stdout.puts "\e[36m Session named: #{name}\e[0m"
197
+ @interactive_old_stdout.puts "\e[36m Session named: #{name}\e[0m"
197
198
  end
198
199
  next
199
200
  end
200
201
 
202
+ # Direct code execution with ">" prefix — skip LLM entirely
203
+ if input.start_with?('>') && !input.start_with?('>=')
204
+ raw_code = input.sub(/\A>\s?/, '')
205
+ Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
206
+ @interactive_console_capture.write("ai> #{input}\n")
207
+
208
+ exec_result = @executor.execute(raw_code)
209
+
210
+ output_parts = []
211
+ output_parts << "Output:\n#{@executor.last_output.strip}" if @executor.last_output && !@executor.last_output.strip.empty?
212
+ output_parts << "Return value: #{exec_result.inspect}" if exec_result
213
+
214
+ result_str = output_parts.join("\n\n")
215
+ result_str = result_str[0..1000] + '...' if result_str.length > 1000
216
+
217
+ context_msg = "User directly executed code: `#{raw_code}`"
218
+ context_msg += "\n#{result_str}" unless output_parts.empty?
219
+ @history << { role: :user, content: context_msg }
220
+
221
+ @interactive_query ||= input
222
+ @last_interactive_code = raw_code
223
+ @last_interactive_output = @executor.last_output
224
+ @last_interactive_result = exec_result ? exec_result.inspect : nil
225
+ @last_interactive_executed = true
226
+
227
+ log_interactive_turn
228
+ next
229
+ end
230
+
201
231
  # Add to Readline history (avoid consecutive duplicates)
202
232
  Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
203
233
 
@@ -328,7 +358,20 @@ module ConsoleAgent
328
358
  $stdout.puts "\e[2m Thinking...\e[0m"
329
359
  end
330
360
 
331
- result = provider.chat_with_tools(messages, tools: tools, system_prompt: context)
361
+ begin
362
+ result = with_escape_monitoring do
363
+ provider.chat_with_tools(messages, tools: tools, system_prompt: context)
364
+ end
365
+ rescue Interrupt
366
+ redirect = prompt_for_redirect
367
+ if redirect
368
+ messages << { role: :user, content: redirect }
369
+ new_messages << messages.last
370
+ next
371
+ else
372
+ raise
373
+ end
374
+ end
332
375
  total_input += result.input_tokens || 0
333
376
  total_output += result.output_tokens || 0
334
377
 
@@ -390,6 +433,59 @@ module ConsoleAgent
390
433
  [final_result, new_messages]
391
434
  end
392
435
 
436
+ # Monitors stdin for Escape (or Ctrl+C, since raw mode disables signals)
437
+ # and raises Interrupt in the main thread when detected.
438
+ def with_escape_monitoring
439
+ require 'io/console'
440
+ return yield unless $stdin.respond_to?(:raw)
441
+
442
+ monitor = Thread.new do
443
+ Thread.current.report_on_exception = false
444
+ $stdin.raw do |io|
445
+ loop do
446
+ break if Thread.current[:stop]
447
+ ready = IO.select([io], nil, nil, 0.2)
448
+ next unless ready
449
+
450
+ char = io.read_nonblock(1) rescue nil
451
+ next unless char
452
+
453
+ if char == "\x03" # Ctrl+C (raw mode eats the signal)
454
+ Thread.main.raise(Interrupt)
455
+ break
456
+ elsif char == "\e"
457
+ # Distinguish standalone Escape from escape sequences (arrow keys, etc.)
458
+ seq = IO.select([io], nil, nil, 0.05)
459
+ if seq
460
+ io.read_nonblock(10) rescue nil # consume the sequence
461
+ else
462
+ Thread.main.raise(Interrupt)
463
+ break
464
+ end
465
+ end
466
+ end
467
+ end
468
+ rescue IOError, Errno::EIO, Errno::ENODEV, Errno::ENOTTY
469
+ # stdin is not a TTY (e.g. in tests or piped input) — silently skip
470
+ end
471
+
472
+ begin
473
+ yield
474
+ ensure
475
+ monitor[:stop] = true
476
+ monitor.join(1) rescue nil
477
+ end
478
+ end
479
+
480
+ def prompt_for_redirect
481
+ $stdout.puts "\n\e[33m Interrupted. What should the AI do differently?\e[0m"
482
+ $stdout.puts "\e[2m (Press Enter with no input to abort entirely)\e[0m"
483
+ $stdout.print "\e[33m redirect> \e[0m"
484
+ input = $stdin.gets
485
+ return nil if input.nil? || input.strip.empty?
486
+ input.strip
487
+ end
488
+
393
489
  def format_tool_args(name, args)
394
490
  return '' if args.nil? || args.empty?
395
491
 
@@ -285,7 +285,7 @@ module ConsoleAgent
285
285
  when 'a', 'auto'
286
286
  skip_confirmations = true
287
287
  when 'y', 'yes'
288
- # proceed with per-step confirmation
288
+ skip_confirmations = true if steps.length == 1
289
289
  else
290
290
  $stdout.puts "\e[33m Plan declined.\e[0m"
291
291
  feedback = ask_feedback("What would you like changed?")
@@ -1,3 +1,3 @@
1
1
  module ConsoleAgent
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr