rails_console_ai 0.17.0 → 0.18.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: 98abad8ed9e3d5d510ffc055352687156ffd4a5c02fbde2610f8ecd46cf2c263
4
- data.tar.gz: 2979eeb2d6f7e3f8df58a4cd25666307cd4e108dfec379020e3ad6422bbba911
3
+ metadata.gz: f6fc753a055c35cdf2aec7ffa98dff0cf5b0dd649e9fd44048d5f43577a69a9a
4
+ data.tar.gz: c9ce948130dae8a58ee8eaaa6d348f00369a9c2f63cb253a9728323493df04e6
5
5
  SHA512:
6
- metadata.gz: bc6695f1321d2c5b39b14e08e676b354f89eb62f29718244a0a25580a5d13250c7581ccfdd52552489f7a53c252433be465fb6b95c9de981825317680f575f5e
7
- data.tar.gz: 35ff574a1c8404cc43c308f5c5bd13bcc1acda339c9543026a7ac216e4756988629088c436a38ab9dac2a34dc2e4a994168a4f8c70a57fbf7262135db8ac8b7d
6
+ metadata.gz: c20f24123aafbef6c1e2dd218ba47f63c3773e0bae332081422aba1f52a821bf5e94baf09a5db13919e61a12cdb40124de139739a352fb9b8486f3cf3de9be77
7
+ data.tar.gz: 62808844bd51a0a5f3aadae221d2a9641b47dc497a548c0edbc1ccda247eaf0e24785920621850b387165d20698f9ccf77aa1f14b59f9432f0c9dcc17c034aac
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.18.0]
6
+
7
+ - Handle "smart" quotes coming from Slack
8
+ - Eager load when Slack bot starts up
9
+ - Handle `>` in Slack for direct code execution
10
+ - Handle stopping of sessions in Slack bot by recording the stop
11
+
5
12
  ## [0.17.0]
6
13
 
7
14
  - Add `/retry` command
@@ -35,6 +35,13 @@ module RailsConsoleAi
35
35
  $stdout = RailsConsoleAi::PrefixedIO.new($stdout) unless $stdout.is_a?(RailsConsoleAi::PrefixedIO)
36
36
  $stderr = RailsConsoleAi::PrefixedIO.new($stderr) unless $stderr.is_a?(RailsConsoleAi::PrefixedIO)
37
37
 
38
+ # Eager load the Rails app so class-level initializers (e.g. Secret.get)
39
+ # run before safety guards are active during user code execution.
40
+ if defined?(Rails) && Rails.application.respond_to?(:eager_load!)
41
+ puts "Eager loading application..."
42
+ Rails.application.eager_load!
43
+ end
44
+
38
45
  @bot_user_id = slack_api("auth.test", token: @bot_token).dig("user_id")
39
46
  log_startup
40
47
 
@@ -272,7 +279,7 @@ module RailsConsoleAi
272
279
  return if event[:user] == @bot_user_id
273
280
  return if event[:subtype]
274
281
 
275
- text = event[:text]
282
+ text = unescape_slack(event[:text])
276
283
  return unless text && !text.strip.empty?
277
284
 
278
285
  channel_id = event[:channel]
@@ -316,6 +323,15 @@ module RailsConsoleAi
316
323
  return
317
324
  end
318
325
 
326
+ # Direct code execution: "> User.count" runs code without LLM
327
+ if text.strip.start_with?('>')
328
+ raw_code = text.strip.sub(/\A>\s*/, '')
329
+ unless raw_code.empty?
330
+ handle_direct_code(session, channel_id, thread_ts, raw_code, user_name)
331
+ return
332
+ end
333
+ end
334
+
319
335
  if session
320
336
  handle_thread_reply(session, text.strip)
321
337
  else
@@ -404,11 +420,76 @@ module RailsConsoleAi
404
420
  end
405
421
  end
406
422
 
423
+ def handle_direct_code(session, channel_id, thread_ts, raw_code, user_name)
424
+ # Ensure a session exists for this thread
425
+ unless session
426
+ start_direct_session(channel_id, thread_ts, user_name)
427
+ session = @mutex.synchronize { @sessions[thread_ts] }
428
+ end
429
+
430
+ channel = session[:channel]
431
+ engine = session[:engine]
432
+
433
+ session[:thread] = Thread.new do
434
+ Thread.current.report_on_exception = false
435
+ Thread.current[:log_prefix] = channel.instance_variable_get(:@log_prefix)
436
+ begin
437
+ engine.execute_direct(raw_code)
438
+ # Post return value to Slack (execute_direct suppresses it via display_result no-op)
439
+ result_value = engine.instance_variable_get(:@last_interactive_result)
440
+ unless result_value.nil?
441
+ display_text = "=> #{result_value}"
442
+ display_text = display_text[0, 3000] + "\n... (truncated)" if display_text.length > 3000
443
+ post_message(channel: channel_id, thread_ts: thread_ts, text: "```#{display_text}```")
444
+ end
445
+ engine.send(:log_interactive_turn)
446
+ rescue => e
447
+ channel.display_error("Error: #{e.class}: #{e.message}")
448
+ RailsConsoleAi.logger.error("SlackBot direct code error: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
449
+ ensure
450
+ ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
451
+ end
452
+ end
453
+ end
454
+
455
+ def start_direct_session(channel_id, thread_ts, user_name)
456
+ channel = Channel::Slack.new(
457
+ slack_bot: self,
458
+ channel_id: channel_id,
459
+ thread_ts: thread_ts,
460
+ user_name: user_name
461
+ )
462
+
463
+ sandbox_binding = Object.new.instance_eval { binding }
464
+ engine = ConversationEngine.new(
465
+ binding_context: sandbox_binding,
466
+ channel: channel,
467
+ slack_thread_ts: thread_ts
468
+ )
469
+
470
+ restore_from_db(engine, thread_ts)
471
+ engine.init_interactive unless engine.instance_variable_get(:@interactive_start)
472
+
473
+ session = { channel: channel, engine: engine, thread: nil }
474
+ @mutex.synchronize { @sessions[thread_ts] = session }
475
+ end
476
+
407
477
  def cancel_session(session, channel_id, thread_ts)
408
478
  if session
409
479
  session[:channel].cancel!
410
480
  session[:channel].display("Stopped.")
411
481
  puts "[#{channel_id}/#{thread_ts}] cancel requested"
482
+
483
+ # Record stop in conversation history so restored sessions know
484
+ # the previous topic was abandoned by the user
485
+ engine = session[:engine]
486
+ engine.history << { role: :user, content: "stop" }
487
+ engine.history << { role: :assistant, content: "Stopped. Awaiting new instructions." }
488
+ begin
489
+ engine.send(:log_interactive_turn)
490
+ rescue => e
491
+ RailsConsoleAi.logger.warn("SlackBot: failed to save cancel state: #{e.message}")
492
+ end
412
493
  else
413
494
  post_message(channel: channel_id, thread_ts: thread_ts, text: "No active session to stop.")
414
495
  puts "[#{channel_id}/#{thread_ts}] cancel: no session"
@@ -454,6 +535,13 @@ module RailsConsoleAi
454
535
  { "ok" => false, "error" => e.message }
455
536
  end
456
537
 
538
+ def unescape_slack(text)
539
+ return text unless text
540
+ text.gsub("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">")
541
+ .gsub("\u2018", "'").gsub("\u2019", "'") # smart single quotes → straight
542
+ .gsub("\u201C", '"').gsub("\u201D", '"') # smart double quotes → straight
543
+ end
544
+
457
545
  def waiting_for_reply?(channel)
458
546
  channel.instance_variable_get(:@reply_queue).num_waiting > 0
459
547
  end
@@ -1,3 +1,3 @@
1
1
  module RailsConsoleAi
2
- VERSION = '0.17.0'.freeze
2
+ VERSION = '0.18.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_console_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr