console_agent 0.1.0 → 0.3.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.
@@ -1,3 +1,5 @@
1
+ require 'readline'
2
+
1
3
  module ConsoleAgent
2
4
  class Repl
3
5
  def initialize(binding_context)
@@ -9,20 +11,48 @@ module ConsoleAgent
9
11
  @history = []
10
12
  @total_input_tokens = 0
11
13
  @total_output_tokens = 0
14
+ @input_history = []
12
15
  end
13
16
 
14
17
  def one_shot(query)
15
- result = send_query(query)
16
- track_usage(result)
17
- code = @executor.display_response(result.text)
18
- display_usage(result)
19
- return nil if code.nil? || code.strip.empty?
20
-
21
- if ConsoleAgent.configuration.auto_execute
22
- @executor.execute(code)
23
- else
24
- @executor.confirm_and_execute(code)
18
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ console_capture = StringIO.new
20
+ exec_result = with_console_capture(console_capture) do
21
+ result, _ = send_query(query)
22
+ track_usage(result)
23
+ code = @executor.display_response(result.text)
24
+ display_usage(result)
25
+
26
+ exec_result = nil
27
+ executed = false
28
+ has_code = code && !code.strip.empty?
29
+
30
+ if has_code
31
+ exec_result = if ConsoleAgent.configuration.auto_execute
32
+ @executor.execute(code)
33
+ else
34
+ @executor.confirm_and_execute(code)
35
+ end
36
+ executed = !@executor.last_cancelled?
37
+ end
38
+
39
+ @_last_log_attrs = {
40
+ query: query,
41
+ conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
42
+ mode: 'one_shot',
43
+ code_executed: has_code ? code : nil,
44
+ code_output: executed ? @executor.last_output : nil,
45
+ code_result: executed && exec_result ? exec_result.inspect : nil,
46
+ executed: executed,
47
+ start_time: start_time
48
+ }
49
+
50
+ exec_result
25
51
  end
52
+
53
+ log_session(@_last_log_attrs.merge(console_output: console_capture.string))
54
+
55
+ exec_result
26
56
  rescue Providers::ProviderError => e
27
57
  $stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
28
58
  nil
@@ -32,10 +62,25 @@ module ConsoleAgent
32
62
  end
33
63
 
34
64
  def explain(query)
35
- result = send_query(query)
36
- track_usage(result)
37
- @executor.display_response(result.text)
38
- display_usage(result)
65
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ console_capture = StringIO.new
67
+ with_console_capture(console_capture) do
68
+ result, _ = send_query(query)
69
+ track_usage(result)
70
+ @executor.display_response(result.text)
71
+ display_usage(result)
72
+
73
+ @_last_log_attrs = {
74
+ query: query,
75
+ conversation: [{ role: :user, content: query }, { role: :assistant, content: result.text }],
76
+ mode: 'explain',
77
+ executed: false,
78
+ start_time: start_time
79
+ }
80
+ end
81
+
82
+ log_session(@_last_log_attrs.merge(console_output: console_capture.string))
83
+
39
84
  nil
40
85
  rescue Providers::ProviderError => e
41
86
  $stderr.puts "\e[31mConsoleAgent Error: #{e.message}\e[0m"
@@ -46,27 +91,143 @@ module ConsoleAgent
46
91
  end
47
92
 
48
93
  def interactive
49
- $stdout.puts "\e[36mConsoleAgent interactive mode. Type 'exit' or 'quit' to leave.\e[0m"
94
+ init_interactive_state
95
+ interactive_loop
96
+ end
97
+
98
+ def resume(session)
99
+ init_interactive_state
100
+
101
+ # Restore state from the previous session
102
+ @history = JSON.parse(session.conversation, symbolize_names: true)
103
+ @interactive_session_id = session.id
104
+ @interactive_query = session.query
105
+ @interactive_session_name = session.name
106
+ @total_input_tokens = session.input_tokens || 0
107
+ @total_output_tokens = session.output_tokens || 0
108
+
109
+ # Replay stored console output so the user sees previous context
110
+ 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
115
+ end
116
+
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
+ interactive_loop
121
+ end
122
+
123
+ private
124
+
125
+ def init_interactive_state
126
+ @interactive_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
127
+ @interactive_console_capture = StringIO.new
128
+ @interactive_old_stdout = $stdout
129
+ $stdout = TeeIO.new(@interactive_old_stdout, @interactive_console_capture)
130
+ @executor.on_prompt = -> { log_interactive_turn }
131
+
50
132
  @history = []
51
133
  @total_input_tokens = 0
52
134
  @total_output_tokens = 0
135
+ @interactive_query = nil
136
+ @interactive_session_id = nil
137
+ @interactive_session_name = nil
138
+ @last_interactive_code = nil
139
+ @last_interactive_output = nil
140
+ @last_interactive_result = nil
141
+ @last_interactive_executed = false
142
+ end
143
+
144
+ def interactive_loop
145
+ auto = ConsoleAgent.configuration.auto_execute
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"
149
+
150
+ # Bind Shift-Tab to insert /auto command and submit
151
+ if Readline.respond_to?(:parse_and_bind)
152
+ Readline.parse_and_bind('"\e[Z": "\C-a\C-k/auto\C-m"')
153
+ end
53
154
 
54
155
  loop do
55
- $stdout.print "\e[33mai> \e[0m"
56
- input = $stdin.gets
57
- break if input.nil?
156
+ input = Readline.readline("\e[33mai> \e[0m", false)
157
+ break if input.nil? # Ctrl-D
58
158
 
59
159
  input = input.strip
60
160
  break if input.downcase == 'exit' || input.downcase == 'quit'
61
161
  next if input.empty?
62
162
 
163
+ if input == '/auto'
164
+ ConsoleAgent.configuration.auto_execute = !ConsoleAgent.configuration.auto_execute
165
+ mode = ConsoleAgent.configuration.auto_execute ? 'ON' : 'OFF'
166
+ $stdout.puts "\e[36m Auto-execute: #{mode}\e[0m"
167
+ next
168
+ end
169
+
170
+ if input == '/usage'
171
+ display_session_summary
172
+ next
173
+ end
174
+
175
+ if input == '/debug'
176
+ ConsoleAgent.configuration.debug = !ConsoleAgent.configuration.debug
177
+ mode = ConsoleAgent.configuration.debug ? 'ON' : 'OFF'
178
+ $stdout.puts "\e[36m Debug: #{mode}\e[0m"
179
+ next
180
+ end
181
+
182
+ if input.start_with?('/name')
183
+ name = input.sub('/name', '').strip
184
+ if name.empty?
185
+ if @interactive_session_name
186
+ $stdout.puts "\e[36m Session name: #{@interactive_session_name}\e[0m"
187
+ else
188
+ $stdout.puts "\e[33m Usage: /name <label> (e.g. /name salesforce_user_123)\e[0m"
189
+ end
190
+ else
191
+ @interactive_session_name = name
192
+ if @interactive_session_id
193
+ require 'console_agent/session_logger'
194
+ SessionLogger.update(@interactive_session_id, name: name)
195
+ end
196
+ $stdout.puts "\e[36m Session named: #{name}\e[0m"
197
+ end
198
+ next
199
+ end
200
+
201
+ # Add to Readline history (avoid consecutive duplicates)
202
+ Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
203
+
204
+ @interactive_query ||= input
63
205
  @history << { role: :user, content: input }
64
206
 
65
- result = send_query(input, conversation: @history)
207
+ # Log the user's prompt line to the console capture (Readline doesn't go through $stdout)
208
+ @interactive_console_capture.write("ai> #{input}\n")
209
+
210
+ # Save immediately so the session is visible in the admin UI while the AI thinks
211
+ log_interactive_turn
212
+
213
+ begin
214
+ result, tool_messages = send_query(input, conversation: @history)
215
+ rescue Interrupt
216
+ $stdout.puts "\n\e[33m Aborted.\e[0m"
217
+ @history.pop # Remove the user message that never got a response
218
+ log_interactive_turn
219
+ next
220
+ end
221
+
66
222
  track_usage(result)
67
223
  code = @executor.display_response(result.text)
68
224
  display_usage(result, show_session: true)
69
225
 
226
+ # Save after response is displayed so viewer shows progress before Execute prompt
227
+ log_interactive_turn
228
+
229
+ # Add tool call/result messages so the LLM remembers what it learned
230
+ @history.concat(tool_messages) if tool_messages && !tool_messages.empty?
70
231
  @history << { role: :assistant, content: result.text }
71
232
 
72
233
  if code && !code.strip.empty?
@@ -76,26 +237,57 @@ module ConsoleAgent
76
237
  exec_result = @executor.confirm_and_execute(code)
77
238
  end
78
239
 
79
- if exec_result
80
- result_str = exec_result.inspect
81
- result_str = result_str[0..500] + '...' if result_str.length > 500
82
- @history << { role: :user, content: "Execution result: #{result_str}" }
240
+ unless @executor.last_cancelled?
241
+ @last_interactive_code = code
242
+ @last_interactive_output = @executor.last_output
243
+ @last_interactive_result = exec_result ? exec_result.inspect : nil
244
+ @last_interactive_executed = true
245
+ end
246
+
247
+ if @executor.last_cancelled?
248
+ @history << { role: :user, content: "User declined to execute the code." }
249
+ else
250
+ output_parts = []
251
+
252
+ # Capture printed output (puts, print, etc.)
253
+ if @executor.last_output && !@executor.last_output.strip.empty?
254
+ output_parts << "Output:\n#{@executor.last_output.strip}"
255
+ end
256
+
257
+ # Capture return value
258
+ if exec_result
259
+ output_parts << "Return value: #{exec_result.inspect}"
260
+ end
261
+
262
+ unless output_parts.empty?
263
+ result_str = output_parts.join("\n\n")
264
+ result_str = result_str[0..1000] + '...' if result_str.length > 1000
265
+ @history << { role: :user, content: "Code was executed. #{result_str}" }
266
+ end
83
267
  end
84
268
  end
269
+
270
+ # Update with the AI response, tokens, and any execution results
271
+ log_interactive_turn
85
272
  end
86
273
 
87
- display_session_summary
88
- $stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
274
+ $stdout = @interactive_old_stdout
275
+ @executor.on_prompt = nil
276
+ finish_interactive_session
277
+ display_exit_info
89
278
  rescue Interrupt
279
+ # Ctrl-C during Readline input — exit cleanly
280
+ $stdout = @interactive_old_stdout if @interactive_old_stdout
281
+ @executor.on_prompt = nil
90
282
  $stdout.puts
91
- display_session_summary
92
- $stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
283
+ finish_interactive_session
284
+ display_exit_info
93
285
  rescue => e
286
+ $stdout = @interactive_old_stdout if @interactive_old_stdout
287
+ @executor.on_prompt = nil
94
288
  $stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
95
289
  end
96
290
 
97
- private
98
-
99
291
  def provider
100
292
  @provider ||= Providers.build
101
293
  end
@@ -112,25 +304,24 @@ module ConsoleAgent
112
304
  ConsoleAgent.configuration.validate!
113
305
 
114
306
  messages = if conversation
115
- conversation.map { |m| { role: m[:role], content: m[:content] } }
307
+ conversation.dup
116
308
  else
117
309
  [{ role: :user, content: query }]
118
310
  end
119
311
 
120
- if ConsoleAgent.configuration.context_mode == :smart
121
- send_query_with_tools(messages)
122
- else
123
- provider.chat(messages, system_prompt: context)
124
- end
312
+ send_query_with_tools(messages)
125
313
  end
126
314
 
127
315
  def send_query_with_tools(messages)
128
316
  require 'console_agent/tools/registry'
129
- tools = Tools::Registry.new
317
+ tools = Tools::Registry.new(executor: @executor)
130
318
  max_rounds = ConsoleAgent.configuration.max_tool_rounds
131
319
  total_input = 0
132
320
  total_output = 0
133
321
  result = nil
322
+ new_messages = [] # Track messages added during tool use
323
+
324
+ exhausted = false
134
325
 
135
326
  max_rounds.times do |round|
136
327
  if round == 0
@@ -149,12 +340,14 @@ module ConsoleAgent
149
340
  end
150
341
 
151
342
  # Add assistant message with tool calls to conversation
152
- messages << provider.format_assistant_message(result)
343
+ assistant_msg = provider.format_assistant_message(result)
344
+ messages << assistant_msg
345
+ new_messages << assistant_msg
153
346
 
154
347
  # Execute each tool and show progress
155
348
  result.tool_calls.each do |tc|
156
- # ask_user handles its own display (prompt + input)
157
- if tc[:name] == 'ask_user'
349
+ # ask_user and execute_plan handle their own display
350
+ if tc[:name] == 'ask_user' || tc[:name] == 'execute_plan'
158
351
  tool_result = tools.execute(tc[:name], tc[:arguments])
159
352
  else
160
353
  args_display = format_tool_args(tc[:name], tc[:arguments])
@@ -163,23 +356,38 @@ module ConsoleAgent
163
356
  tool_result = tools.execute(tc[:name], tc[:arguments])
164
357
 
165
358
  preview = compact_tool_result(tc[:name], tool_result)
166
- $stdout.puts "\e[2m #{preview}\e[0m"
359
+ cached_tag = tools.last_cached? ? " (cached)" : ""
360
+ $stdout.puts "\e[2m #{preview}#{cached_tag}\e[0m"
167
361
  end
168
362
 
169
363
  if ConsoleAgent.configuration.debug
170
364
  $stderr.puts "\e[35m[debug tool result] #{tool_result}\e[0m"
171
365
  end
172
366
 
173
- messages << provider.format_tool_result(tc[:id], tool_result)
367
+ tool_msg = provider.format_tool_result(tc[:id], tool_result)
368
+ messages << tool_msg
369
+ new_messages << tool_msg
174
370
  end
371
+
372
+ exhausted = true if round == max_rounds - 1
373
+ end
374
+
375
+ # If we hit the tool round limit, force a final response without tools
376
+ if exhausted
377
+ $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"
378
+ 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." }
379
+ result = provider.chat(messages, system_prompt: context)
380
+ total_input += result.input_tokens || 0
381
+ total_output += result.output_tokens || 0
175
382
  end
176
383
 
177
- Providers::ChatResult.new(
384
+ final_result = Providers::ChatResult.new(
178
385
  text: result ? result.text : '',
179
386
  input_tokens: total_input,
180
387
  output_tokens: total_output,
181
388
  stop_reason: result ? result.stop_reason : :end_turn
182
389
  )
390
+ [final_result, new_messages]
183
391
  end
184
392
 
185
393
  def format_tool_args(name, args)
@@ -197,6 +405,15 @@ module ConsoleAgent
197
405
  "(\"#{args['query']}\"#{dir})"
198
406
  when 'list_files'
199
407
  args['directory'] ? "(\"#{args['directory']}\")" : ''
408
+ when 'save_memory'
409
+ "(\"#{args['name']}\")"
410
+ when 'delete_memory'
411
+ "(\"#{args['name']}\")"
412
+ when 'recall_memories'
413
+ args['query'] ? "(\"#{args['query']}\")" : ''
414
+ when 'execute_plan'
415
+ steps = args['steps']
416
+ steps ? "(#{steps.length} steps)" : ''
200
417
  else
201
418
  ''
202
419
  end
@@ -244,6 +461,16 @@ module ConsoleAgent
244
461
  else
245
462
  truncate(result, 80)
246
463
  end
464
+ when 'save_memory'
465
+ (result.start_with?('Memory saved') || result.start_with?('Memory updated')) ? result : truncate(result, 80)
466
+ when 'delete_memory'
467
+ result.start_with?('Memory deleted') ? result : truncate(result, 80)
468
+ when 'recall_memories'
469
+ chunks = result.split("\n\n")
470
+ chunks.length > 1 ? "#{chunks.length} memories found" : truncate(result, 80)
471
+ when 'execute_plan'
472
+ steps_done = result.scan(/^Step \d+/).length
473
+ steps_done > 0 ? "#{steps_done} steps executed" : truncate(result, 80)
247
474
  else
248
475
  truncate(result, 80)
249
476
  end
@@ -277,10 +504,104 @@ module ConsoleAgent
277
504
  $stdout.puts line
278
505
  end
279
506
 
507
+ def with_console_capture(capture_io)
508
+ old_stdout = $stdout
509
+ $stdout = TeeIO.new(old_stdout, capture_io)
510
+ yield
511
+ ensure
512
+ $stdout = old_stdout
513
+ end
514
+
515
+ def log_interactive_turn
516
+ require 'console_agent/session_logger'
517
+ session_attrs = {
518
+ conversation: @history,
519
+ input_tokens: @total_input_tokens,
520
+ output_tokens: @total_output_tokens,
521
+ code_executed: @last_interactive_code,
522
+ code_output: @last_interactive_output,
523
+ code_result: @last_interactive_result,
524
+ executed: @last_interactive_executed,
525
+ console_output: @interactive_console_capture&.string
526
+ }
527
+
528
+ if @interactive_session_id
529
+ SessionLogger.update(@interactive_session_id, session_attrs)
530
+ else
531
+ @interactive_session_id = SessionLogger.log(
532
+ session_attrs.merge(
533
+ query: @interactive_query || '(interactive session)',
534
+ mode: 'interactive',
535
+ name: @interactive_session_name
536
+ )
537
+ )
538
+ end
539
+ end
540
+
541
+ def finish_interactive_session
542
+ require 'console_agent/session_logger'
543
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @interactive_start) * 1000).round
544
+ if @interactive_session_id
545
+ SessionLogger.update(@interactive_session_id,
546
+ conversation: @history,
547
+ input_tokens: @total_input_tokens,
548
+ output_tokens: @total_output_tokens,
549
+ code_executed: @last_interactive_code,
550
+ code_output: @last_interactive_output,
551
+ code_result: @last_interactive_result,
552
+ executed: @last_interactive_executed,
553
+ console_output: @interactive_console_capture&.string,
554
+ duration_ms: duration_ms
555
+ )
556
+ elsif @interactive_query
557
+ # Session was never created (e.g., only one turn that failed to log)
558
+ log_session(
559
+ query: @interactive_query,
560
+ conversation: @history,
561
+ mode: 'interactive',
562
+ code_executed: @last_interactive_code,
563
+ code_output: @last_interactive_output,
564
+ code_result: @last_interactive_result,
565
+ executed: @last_interactive_executed,
566
+ console_output: @interactive_console_capture&.string,
567
+ start_time: @interactive_start
568
+ )
569
+ end
570
+ end
571
+
572
+ def log_session(attrs)
573
+ require 'console_agent/session_logger'
574
+ start_time = attrs.delete(:start_time)
575
+ duration_ms = if start_time
576
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
577
+ end
578
+ SessionLogger.log(
579
+ attrs.merge(
580
+ input_tokens: @total_input_tokens,
581
+ output_tokens: @total_output_tokens,
582
+ duration_ms: duration_ms
583
+ )
584
+ )
585
+ end
586
+
280
587
  def display_session_summary
281
588
  return if @total_input_tokens == 0 && @total_output_tokens == 0
282
589
 
283
590
  $stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
284
591
  end
592
+
593
+ def display_exit_info
594
+ display_session_summary
595
+ if @interactive_session_id
596
+ $stdout.puts "\e[36mSession ##{@interactive_session_id} saved.\e[0m"
597
+ if @interactive_session_name
598
+ $stdout.puts "\e[2m Resume with: ai_resume \"#{@interactive_session_name}\"\e[0m"
599
+ else
600
+ $stdout.puts "\e[2m Name it: ai_name #{@interactive_session_id}, \"descriptive_name\"\e[0m"
601
+ $stdout.puts "\e[2m Resume it: ai_resume #{@interactive_session_id}\e[0m"
602
+ end
603
+ end
604
+ $stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
605
+ end
285
606
  end
286
607
  end
@@ -0,0 +1,79 @@
1
+ module ConsoleAgent
2
+ module SessionLogger
3
+ class << self
4
+ def log(attrs)
5
+ return unless ConsoleAgent.configuration.session_logging
6
+ return unless table_exists?
7
+
8
+ record = session_class.create!(
9
+ query: attrs[:query],
10
+ conversation: Array(attrs[:conversation]).to_json,
11
+ input_tokens: attrs[:input_tokens] || 0,
12
+ output_tokens: attrs[:output_tokens] || 0,
13
+ user_name: current_user_name,
14
+ mode: attrs[:mode].to_s,
15
+ name: attrs[:name],
16
+ code_executed: attrs[:code_executed],
17
+ code_output: attrs[:code_output],
18
+ code_result: attrs[:code_result],
19
+ console_output: attrs[:console_output],
20
+ executed: attrs[:executed] || false,
21
+ provider: ConsoleAgent.configuration.provider.to_s,
22
+ model: ConsoleAgent.configuration.resolved_model,
23
+ duration_ms: attrs[:duration_ms],
24
+ created_at: Time.respond_to?(:current) ? Time.current : Time.now
25
+ )
26
+ record.id
27
+ rescue => e
28
+ msg = "ConsoleAgent: session logging failed: #{e.class}: #{e.message}"
29
+ $stderr.puts "\e[33m#{msg}\e[0m" if $stderr.respond_to?(:puts)
30
+ ConsoleAgent.logger.warn(msg)
31
+ nil
32
+ end
33
+
34
+ def update(id, attrs)
35
+ return unless id
36
+ return unless ConsoleAgent.configuration.session_logging
37
+ return unless table_exists?
38
+
39
+ updates = {}
40
+ updates[:conversation] = Array(attrs[:conversation]).to_json if attrs.key?(:conversation)
41
+ updates[:input_tokens] = attrs[:input_tokens] if attrs.key?(:input_tokens)
42
+ updates[:output_tokens] = attrs[:output_tokens] if attrs.key?(:output_tokens)
43
+ updates[:code_executed] = attrs[:code_executed] if attrs.key?(:code_executed)
44
+ updates[:code_output] = attrs[:code_output] if attrs.key?(:code_output)
45
+ updates[:code_result] = attrs[:code_result] if attrs.key?(:code_result)
46
+ updates[:console_output] = attrs[:console_output] if attrs.key?(:console_output)
47
+ updates[:executed] = attrs[:executed] if attrs.key?(:executed)
48
+ updates[:duration_ms] = attrs[:duration_ms] if attrs.key?(:duration_ms)
49
+ updates[:name] = attrs[:name] if attrs.key?(:name)
50
+
51
+ session_class.where(id: id).update_all(updates) unless updates.empty?
52
+ rescue => e
53
+ msg = "ConsoleAgent: session update failed: #{e.class}: #{e.message}"
54
+ $stderr.puts "\e[33m#{msg}\e[0m" if $stderr.respond_to?(:puts)
55
+ ConsoleAgent.logger.warn(msg)
56
+ nil
57
+ end
58
+
59
+ private
60
+
61
+ def table_exists?
62
+ # Only cache positive results — retry on failure so transient
63
+ # errors (boot timing, connection not ready) don't stick forever
64
+ return true if @table_exists
65
+ @table_exists = session_class.connection.table_exists?('console_agent_sessions')
66
+ rescue
67
+ false
68
+ end
69
+
70
+ def session_class
71
+ Object.const_get('ConsoleAgent::Session')
72
+ end
73
+
74
+ def current_user_name
75
+ ConsoleAgent.current_user || ENV['USER']
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,27 @@
1
+ module ConsoleAgent
2
+ module Storage
3
+ class StorageError < StandardError; end
4
+
5
+ class Base
6
+ def read(key)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def write(key, content)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def list(pattern)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def exists?(key)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def delete(key)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end