smart_message 0.0.1

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/.gitignore +8 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +100 -0
  6. data/COMMITS.md +196 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +71 -0
  9. data/README.md +303 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/docs/README.md +52 -0
  14. data/docs/architecture.md +370 -0
  15. data/docs/dispatcher.md +593 -0
  16. data/docs/examples.md +808 -0
  17. data/docs/getting-started.md +235 -0
  18. data/docs/ideas_to_think_about.md +329 -0
  19. data/docs/serializers.md +575 -0
  20. data/docs/transports.md +501 -0
  21. data/docs/troubleshooting.md +582 -0
  22. data/examples/01_point_to_point_orders.rb +200 -0
  23. data/examples/02_publish_subscribe_events.rb +364 -0
  24. data/examples/03_many_to_many_chat.rb +608 -0
  25. data/examples/README.md +335 -0
  26. data/examples/tmux_chat/README.md +283 -0
  27. data/examples/tmux_chat/bot_agent.rb +272 -0
  28. data/examples/tmux_chat/human_agent.rb +197 -0
  29. data/examples/tmux_chat/room_monitor.rb +158 -0
  30. data/examples/tmux_chat/shared_chat_system.rb +295 -0
  31. data/examples/tmux_chat/start_chat_demo.sh +190 -0
  32. data/examples/tmux_chat/stop_chat_demo.sh +22 -0
  33. data/lib/simple_stats.rb +57 -0
  34. data/lib/smart_message/base.rb +284 -0
  35. data/lib/smart_message/dispatcher/.keep +0 -0
  36. data/lib/smart_message/dispatcher.rb +146 -0
  37. data/lib/smart_message/errors.rb +29 -0
  38. data/lib/smart_message/header.rb +20 -0
  39. data/lib/smart_message/logger/base.rb +8 -0
  40. data/lib/smart_message/logger.rb +7 -0
  41. data/lib/smart_message/serializer/base.rb +23 -0
  42. data/lib/smart_message/serializer/json.rb +22 -0
  43. data/lib/smart_message/serializer.rb +10 -0
  44. data/lib/smart_message/transport/base.rb +85 -0
  45. data/lib/smart_message/transport/memory_transport.rb +69 -0
  46. data/lib/smart_message/transport/registry.rb +59 -0
  47. data/lib/smart_message/transport/stdout_transport.rb +62 -0
  48. data/lib/smart_message/transport.rb +41 -0
  49. data/lib/smart_message/version.rb +7 -0
  50. data/lib/smart_message/wrapper.rb +43 -0
  51. data/lib/smart_message.rb +54 -0
  52. data/smart_message.gemspec +53 -0
  53. metadata +252 -0
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/tmux_chat/bot_agent.rb
3
+ #
4
+ # Bot agent for tmux chat visualization
5
+
6
+ require_relative 'shared_chat_system'
7
+
8
+ class BotChatAgent < BaseAgent
9
+ def initialize(bot_id:, name:, capabilities: [])
10
+ @capabilities = capabilities
11
+ @command_count = 0
12
+ super(agent_id: bot_id, name: name, agent_type: 'bot')
13
+
14
+ log_display("🤖 Bot #{@name} online!")
15
+ log_display("🔧 Capabilities: #{@capabilities.join(', ')}")
16
+ log_display("⚡ Listening for commands and messages...")
17
+ log_display("")
18
+ end
19
+
20
+ def setup_subscriptions
21
+ # Subscribe to bot commands and chat messages
22
+ BotCommandMessage.subscribe("BotChatAgent.handle_bot_command_#{@agent_id}")
23
+ ChatMessage.subscribe("BotChatAgent.handle_chat_message_#{@agent_id}")
24
+
25
+ # Register this instance
26
+ @@bots ||= {}
27
+ @@bots[@agent_id] = self
28
+ end
29
+
30
+ def run
31
+ # Start processing messages and wait
32
+ begin
33
+ while true
34
+ sleep(1)
35
+ end
36
+ rescue Interrupt
37
+ shutdown
38
+ end
39
+ end
40
+
41
+ # Class method routing for SmartMessage
42
+ def self.method_missing(method_name, *args)
43
+ if method_name.to_s.start_with?('handle_bot_command_')
44
+ bot_id = method_name.to_s.split('_').last
45
+ bot = (@@bots ||= {})[bot_id]
46
+ bot&.handle_bot_command(*args)
47
+ elsif method_name.to_s.start_with?('handle_chat_message_')
48
+ bot_id = method_name.to_s.split('_').last
49
+ bot = (@@bots ||= {})[bot_id]
50
+ bot&.handle_chat_message(*args)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def handle_bot_command(message_header, message_payload)
57
+ command_data = JSON.parse(message_payload)
58
+
59
+ # Only handle commands in rooms we're in and commands we can handle
60
+ return unless @active_rooms.include?(command_data['room_id'])
61
+ return unless can_handle_command?(command_data['command'])
62
+
63
+ @command_count += 1
64
+ log_display("⚡ Processing command: /#{command_data['command']} (#{@command_count})")
65
+
66
+ process_command(command_data)
67
+ end
68
+
69
+ def handle_chat_message(message_header, message_payload)
70
+ chat_data = JSON.parse(message_payload)
71
+
72
+ # Only process messages from rooms we're in and not our own messages
73
+ return unless @active_rooms.include?(chat_data['room_id'])
74
+ return if chat_data['sender_id'] == @agent_id
75
+
76
+ # Log the message
77
+ log_display("👁️ [#{chat_data['room_id']}] #{chat_data['sender_name']}: #{chat_data['content']}")
78
+
79
+ # Check if it's a bot command
80
+ if chat_data['content'].start_with?('/')
81
+ handle_inline_command(chat_data)
82
+ else
83
+ # Respond to certain keywords
84
+ respond_to_keywords(chat_data)
85
+ end
86
+ end
87
+
88
+ def can_handle_command?(command)
89
+ @capabilities.include?(command)
90
+ end
91
+
92
+ private
93
+
94
+ def handle_inline_command(chat_data)
95
+ content = chat_data['content']
96
+ command_parts = content[1..-1].split(' ')
97
+ command = command_parts.first
98
+ parameters = command_parts[1..-1]
99
+
100
+ return unless can_handle_command?(command)
101
+
102
+ # Create bot command message
103
+ bot_command = BotCommandMessage.new(
104
+ command_id: "CMD-#{@agent_id}-#{Time.now.to_i}-#{rand(1000)}",
105
+ room_id: chat_data['room_id'],
106
+ user_id: chat_data['sender_id'],
107
+ user_name: chat_data['sender_name'],
108
+ command: command,
109
+ parameters: parameters,
110
+ timestamp: Time.now.iso8601
111
+ )
112
+
113
+ bot_command.publish
114
+ end
115
+
116
+ def process_command(command_data)
117
+ case command_data['command']
118
+ when 'weather'
119
+ handle_weather_command(command_data)
120
+ when 'joke'
121
+ handle_joke_command(command_data)
122
+ when 'help'
123
+ handle_help_command(command_data)
124
+ when 'stats'
125
+ handle_stats_command(command_data)
126
+ when 'time'
127
+ handle_time_command(command_data)
128
+ when 'echo'
129
+ handle_echo_command(command_data)
130
+ else
131
+ send_bot_response(
132
+ room_id: command_data['room_id'],
133
+ content: "🤷‍♂️ Sorry, I don't know how to handle /#{command_data['command']}"
134
+ )
135
+ end
136
+ end
137
+
138
+ def respond_to_keywords(chat_data)
139
+ content = chat_data['content'].downcase
140
+
141
+ if content.include?('hello') || content.include?('hi')
142
+ send_bot_response(
143
+ room_id: chat_data['room_id'],
144
+ content: "Hello #{chat_data['sender_name']}! 👋"
145
+ )
146
+ elsif content.include?('help') && !content.start_with?('/')
147
+ send_bot_response(
148
+ room_id: chat_data['room_id'],
149
+ content: "Type /help to see my commands! 🤖"
150
+ )
151
+ elsif content.include?('thank')
152
+ send_bot_response(
153
+ room_id: chat_data['room_id'],
154
+ content: "You're welcome! 😊"
155
+ )
156
+ end
157
+ end
158
+
159
+ def handle_weather_command(command_data)
160
+ location = command_data['parameters'].first || 'your location'
161
+ weather_responses = [
162
+ "☀️ It's sunny and 72°F in #{location}!",
163
+ "🌧️ Looks like rain and 65°F in #{location}",
164
+ "❄️ Snow expected, 32°F in #{location}",
165
+ "⛅ Partly cloudy, 68°F in #{location}",
166
+ "🌪️ Tornado warning in #{location}! (Just kidding, it's nice)"
167
+ ]
168
+
169
+ # Simulate API delay
170
+ Thread.new do
171
+ sleep(0.5)
172
+ send_bot_response(
173
+ room_id: command_data['room_id'],
174
+ content: weather_responses.sample
175
+ )
176
+ end
177
+ end
178
+
179
+ def handle_joke_command(command_data)
180
+ jokes = [
181
+ "Why don't scientists trust atoms? Because they make up everything! 😄",
182
+ "Why did the scarecrow win an award? He was outstanding in his field! 🌾",
183
+ "What do you call a fake noodle? An impasta! 🍝",
184
+ "Why don't eggs tell jokes? They'd crack each other up! 🥚",
185
+ "What do you call a sleeping bull? A bulldozer! 😴",
186
+ "Why don't robots ever panic? They have enough bytes! 🤖"
187
+ ]
188
+
189
+ send_bot_response(
190
+ room_id: command_data['room_id'],
191
+ content: jokes.sample
192
+ )
193
+ end
194
+
195
+ def handle_help_command(command_data)
196
+ help_text = @capabilities.map do |cmd|
197
+ " /#{cmd} - #{get_command_description(cmd)}"
198
+ end.join("\n")
199
+
200
+ send_bot_response(
201
+ room_id: command_data['room_id'],
202
+ content: "🤖 #{@name} Commands:\n#{help_text}"
203
+ )
204
+ end
205
+
206
+ def handle_stats_command(command_data)
207
+ send_bot_response(
208
+ room_id: command_data['room_id'],
209
+ content: "📊 Bot Stats:\n" +
210
+ " • Active rooms: #{@active_rooms.length}\n" +
211
+ " • Commands processed: #{@command_count}\n" +
212
+ " • Capabilities: #{@capabilities.length}\n" +
213
+ " • Uptime: #{Time.now.strftime('%H:%M:%S')}"
214
+ )
215
+ end
216
+
217
+ def handle_time_command(command_data)
218
+ timezone = command_data['parameters'].first || 'local'
219
+ current_time = Time.now.strftime('%Y-%m-%d %H:%M:%S')
220
+
221
+ send_bot_response(
222
+ room_id: command_data['room_id'],
223
+ content: "🕒 Current time (#{timezone}): #{current_time}"
224
+ )
225
+ end
226
+
227
+ def handle_echo_command(command_data)
228
+ message = command_data['parameters'].join(' ')
229
+ if message.empty?
230
+ message = "Echo! Echo! Echo! 📢"
231
+ end
232
+
233
+ send_bot_response(
234
+ room_id: command_data['room_id'],
235
+ content: "🔊 Echo: #{message}"
236
+ )
237
+ end
238
+
239
+ def get_command_description(command)
240
+ descriptions = {
241
+ 'weather' => 'Get weather information',
242
+ 'joke' => 'Tell a random joke',
243
+ 'help' => 'Show this help message',
244
+ 'stats' => 'Show bot statistics',
245
+ 'time' => 'Show current time',
246
+ 'echo' => 'Echo your message'
247
+ }
248
+
249
+ descriptions[command] || 'No description available'
250
+ end
251
+
252
+ def send_bot_response(room_id:, content:)
253
+ send_message(room_id: room_id, content: content, message_type: 'bot')
254
+ log_display("💬 Replied to [#{room_id}]: #{content}")
255
+ end
256
+ end
257
+
258
+ # Main execution
259
+ if __FILE__ == $0
260
+ if ARGV.length < 2
261
+ puts "Usage: #{$0} <bot_id> <name> [capability1,capability2,...]"
262
+ puts "Example: #{$0} helpbot HelpBot help,stats,time"
263
+ exit 1
264
+ end
265
+
266
+ bot_id = ARGV[0]
267
+ name = ARGV[1]
268
+ capabilities = ARGV[2] ? ARGV[2].split(',') : ['help']
269
+
270
+ bot = BotChatAgent.new(bot_id: bot_id, name: name, capabilities: capabilities)
271
+ bot.run
272
+ end
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/tmux_chat/human_agent.rb
3
+ #
4
+ # Human chat agent for tmux visualization
5
+
6
+ require_relative 'shared_chat_system'
7
+
8
+ begin
9
+ require 'io/console'
10
+ rescue LoadError
11
+ # Console methods may not be available, handled gracefully
12
+ end
13
+
14
+ class HumanChatAgent < BaseAgent
15
+ def initialize(user_id:, name:)
16
+ @message_counter = 0
17
+ super(agent_id: user_id, name: name, agent_type: 'human')
18
+
19
+ log_display("👤 Human agent #{@name} ready!")
20
+ log_display("Commands: /join <room>, /leave <room>, /list, /quit")
21
+ log_display("Type messages to send to current rooms")
22
+ log_display("")
23
+ end
24
+
25
+ def setup_subscriptions
26
+ # Subscribe to chat messages
27
+ ChatMessage.subscribe("HumanChatAgent.handle_chat_message_#{@agent_id}")
28
+ SystemNotificationMessage.subscribe("HumanChatAgent.handle_system_notification_#{@agent_id}")
29
+
30
+ # Register this instance for method dispatch
31
+ @@agents ||= {}
32
+ @@agents[@agent_id] = self
33
+ end
34
+
35
+ def start_interactive_session
36
+ while true
37
+ begin
38
+ print "> "
39
+ input = STDIN.gets&.chomp
40
+ break if input.nil?
41
+
42
+ next if input.strip.empty?
43
+
44
+ if input.start_with?('/')
45
+ handle_command(input)
46
+ else
47
+ send_to_active_rooms(input)
48
+ end
49
+ rescue Interrupt
50
+ break
51
+ end
52
+ end
53
+
54
+ shutdown
55
+ end
56
+
57
+ # Class method routing for SmartMessage
58
+ def self.method_missing(method_name, *args)
59
+ if method_name.to_s.start_with?('handle_chat_message_')
60
+ user_id = method_name.to_s.split('_').last
61
+ agent = (@@agents ||= {})[user_id]
62
+ agent&.handle_chat_message(*args)
63
+ elsif method_name.to_s.start_with?('handle_system_notification_')
64
+ user_id = method_name.to_s.split('_').last
65
+ agent = (@@agents ||= {})[user_id]
66
+ agent&.handle_system_notification(*args)
67
+ else
68
+ super
69
+ end
70
+ end
71
+
72
+ def handle_chat_message(message_header, message_payload)
73
+ chat_data = JSON.parse(message_payload)
74
+
75
+ # Only process messages from rooms we're in and not our own messages
76
+ return unless @active_rooms.include?(chat_data['room_id'])
77
+ return if chat_data['sender_id'] == @agent_id
78
+
79
+ sender_emoji = case chat_data['message_type']
80
+ when 'bot' then '🤖'
81
+ when 'system' then '🔔'
82
+ else '👤'
83
+ end
84
+
85
+ log_display("#{sender_emoji} [#{chat_data['room_id']}] #{chat_data['sender_name']}: #{chat_data['content']}")
86
+
87
+ # Auto-respond if mentioned
88
+ if chat_data['mentions']&.include?(@name.downcase) || chat_data['content'].include?("@#{@name}")
89
+ respond_to_mention(chat_data)
90
+ end
91
+ end
92
+
93
+ def handle_system_notification(message_header, message_payload)
94
+ notif_data = JSON.parse(message_payload)
95
+
96
+ # Only process notifications from rooms we're in
97
+ return unless @active_rooms.include?(notif_data['room_id'])
98
+
99
+ log_display("🔔 [#{notif_data['room_id']}] #{notif_data['content']}")
100
+ end
101
+
102
+ private
103
+
104
+ def handle_command(input)
105
+ parts = input[1..-1].split(' ')
106
+ command = parts[0]
107
+ args = parts[1..-1]
108
+
109
+ case command
110
+ when 'join'
111
+ if args.empty?
112
+ log_display("❌ Usage: /join <room_id>")
113
+ else
114
+ join_room(args[0])
115
+ update_display_header
116
+ end
117
+ when 'leave'
118
+ if args.empty?
119
+ log_display("❌ Usage: /leave <room_id>")
120
+ else
121
+ leave_room(args[0])
122
+ update_display_header
123
+ end
124
+ when 'list'
125
+ log_display("📋 Active rooms: #{@active_rooms.empty? ? 'none' : @active_rooms.join(', ')}")
126
+ when 'quit', 'exit'
127
+ log_display("👋 Goodbye!")
128
+ exit(0)
129
+ when 'help'
130
+ log_display("📖 Commands:")
131
+ log_display(" /join <room> - Join a chat room")
132
+ log_display(" /leave <room> - Leave a chat room")
133
+ log_display(" /list - List active rooms")
134
+ log_display(" /quit - Exit the chat")
135
+ else
136
+ log_display("❌ Unknown command: /#{command}. Type /help for help.")
137
+ end
138
+ end
139
+
140
+ def send_to_active_rooms(message)
141
+ if @active_rooms.empty?
142
+ log_display("❌ You're not in any rooms. Use /join <room> to join a room.")
143
+ return
144
+ end
145
+
146
+ @active_rooms.each do |room_id|
147
+ send_message(room_id: room_id, content: message)
148
+ end
149
+ end
150
+
151
+ def respond_to_mention(chat_data)
152
+ responses = [
153
+ "Thanks for mentioning me!",
154
+ "I'm here, what's up?",
155
+ "How can I help?",
156
+ "Yes, I saw that!",
157
+ "Interesting point!"
158
+ ]
159
+
160
+ # Delay response slightly to make it feel natural
161
+ Thread.new do
162
+ sleep(0.5 + rand)
163
+ send_message(
164
+ room_id: chat_data['room_id'],
165
+ content: responses.sample
166
+ )
167
+ end
168
+ end
169
+
170
+ def update_display_header
171
+ # Move cursor to update rooms line
172
+ print "\033[4;9H" # Move to line 4, column 9
173
+ print "#{@active_rooms.join(', ').ljust(42)}"
174
+ # Move cursor to bottom (default to 40 lines if console unavailable)
175
+ max_lines = begin
176
+ IO.console&.winsize&.first || 40
177
+ rescue
178
+ 40
179
+ end
180
+ print "\033[#{max_lines};1H"
181
+ end
182
+ end
183
+
184
+ # Main execution
185
+ if __FILE__ == $0
186
+ if ARGV.length < 2
187
+ puts "Usage: #{$0} <user_id> <name>"
188
+ puts "Example: #{$0} alice Alice"
189
+ exit 1
190
+ end
191
+
192
+ user_id = ARGV[0]
193
+ name = ARGV[1]
194
+
195
+ agent = HumanChatAgent.new(user_id: user_id, name: name)
196
+ agent.start_interactive_session
197
+ end
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/tmux_chat/room_monitor.rb
3
+ #
4
+ # Room activity monitor for tmux chat visualization
5
+
6
+ require_relative 'shared_chat_system'
7
+
8
+ begin
9
+ require 'io/console'
10
+ rescue LoadError
11
+ # Console methods may not be available, handled gracefully
12
+ end
13
+
14
+ class RoomMonitor < BaseAgent
15
+ def initialize(room_id)
16
+ @room_id = room_id
17
+ @message_count = 0
18
+ @user_count = 0
19
+ @last_activity = Time.now
20
+ @participants = Set.new
21
+
22
+ super(agent_id: "monitor-#{room_id}", name: "Room Monitor", agent_type: 'monitor')
23
+
24
+ join_room(@room_id)
25
+ log_display("🏠 Monitoring room: #{@room_id}")
26
+ log_display("📊 Waiting for activity...")
27
+ log_display("")
28
+ end
29
+
30
+ def setup_subscriptions
31
+ ChatMessage.subscribe("RoomMonitor.handle_chat_message_#{@agent_id}")
32
+ SystemNotificationMessage.subscribe("RoomMonitor.handle_system_notification_#{@agent_id}")
33
+
34
+ @@monitors ||= {}
35
+ @@monitors[@agent_id] = self
36
+ end
37
+
38
+ def run
39
+ # Update display every few seconds
40
+ begin
41
+ while true
42
+ sleep(2)
43
+ update_stats_display
44
+ end
45
+ rescue Interrupt
46
+ shutdown
47
+ end
48
+ end
49
+
50
+ # Class method routing
51
+ def self.method_missing(method_name, *args)
52
+ if method_name.to_s.start_with?('handle_chat_message_')
53
+ monitor_id = method_name.to_s.split('_', 4).last
54
+ monitor = (@@monitors ||= {})[monitor_id]
55
+ monitor&.handle_chat_message(*args)
56
+ elsif method_name.to_s.start_with?('handle_system_notification_')
57
+ monitor_id = method_name.to_s.split('_', 4).last
58
+ monitor = (@@monitors ||= {})[monitor_id]
59
+ monitor&.handle_system_notification(*args)
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def handle_chat_message(message_header, message_payload)
66
+ chat_data = JSON.parse(message_payload)
67
+
68
+ return unless chat_data['room_id'] == @room_id
69
+ return if chat_data['sender_id'] == @agent_id
70
+
71
+ @message_count += 1
72
+ @last_activity = Time.now
73
+ @participants.add(chat_data['sender_name'])
74
+
75
+ sender_emoji = case chat_data['message_type']
76
+ when 'bot' then '🤖'
77
+ when 'system' then '🔔'
78
+ else '👤'
79
+ end
80
+
81
+ # Show the message with metadata
82
+ timestamp = Time.now.strftime("%H:%M:%S")
83
+ log_display("#{sender_emoji} [#{timestamp}] #{chat_data['sender_name']}: #{chat_data['content']}")
84
+
85
+ # Check for commands
86
+ if chat_data['content'].start_with?('/')
87
+ log_display("⚡ Command detected: #{chat_data['content']}")
88
+ end
89
+
90
+ # Check for mentions
91
+ if chat_data['mentions'] && !chat_data['mentions'].empty?
92
+ log_display("🏷️ Mentions: #{chat_data['mentions'].join(', ')}")
93
+ end
94
+ end
95
+
96
+ def handle_system_notification(message_header, message_payload)
97
+ notif_data = JSON.parse(message_payload)
98
+
99
+ return unless notif_data['room_id'] == @room_id
100
+
101
+ case notif_data['notification_type']
102
+ when 'user_joined'
103
+ @user_count += 1
104
+ log_display("📥 #{notif_data['content']}")
105
+ when 'user_left'
106
+ @user_count = [@user_count - 1, 0].max
107
+ log_display("📤 #{notif_data['content']}")
108
+ else
109
+ log_display("🔔 #{notif_data['content']}")
110
+ end
111
+
112
+ @last_activity = Time.now
113
+ end
114
+
115
+ private
116
+
117
+ def update_stats_display
118
+ # Move cursor to update stats
119
+ time_since_activity = Time.now - @last_activity
120
+ activity_status = if time_since_activity < 10
121
+ "🟢 Active"
122
+ elsif time_since_activity < 60
123
+ "🟡 Quiet (#{time_since_activity.to_i}s ago)"
124
+ else
125
+ "🔴 Inactive (#{(time_since_activity / 60).to_i}m ago)"
126
+ end
127
+
128
+ # Update the header area with stats
129
+ print "\033[2;2H" # Move to line 2
130
+ room_info = " ROOM: #{@room_id} | #{activity_status} "
131
+ print room_info.center(50)
132
+
133
+ print "\033[4;2H" # Move to line 4
134
+ stats_info = " Messages: #{@message_count} | Participants: #{@participants.size} "
135
+ print stats_info.center(50)
136
+
137
+ # Move cursor back to bottom (default to 40 lines if console unavailable)
138
+ max_lines = begin
139
+ IO.console&.winsize&.first || 40
140
+ rescue
141
+ 40
142
+ end
143
+ print "\033[#{max_lines};1H"
144
+ end
145
+ end
146
+
147
+ # Main execution
148
+ if __FILE__ == $0
149
+ if ARGV.empty?
150
+ puts "Usage: #{$0} <room_id>"
151
+ puts "Example: #{$0} general"
152
+ exit 1
153
+ end
154
+
155
+ room_id = ARGV[0]
156
+ monitor = RoomMonitor.new(room_id)
157
+ monitor.run
158
+ end