brainiac-discord 0.0.3 → 0.0.4

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: f25b1975521590991edab7777fc07cb22083c2c151998f42bc4f0151bc6b5f11
4
- data.tar.gz: ba4aeb7c4559d80bc0fea056ec8e0505ad884ffa5ac6945e648d830f357e2b9e
3
+ metadata.gz: cdd8beafe4070560e4caaaa465d2b1e9dde33defa1b7fdc429467864776d3c21
4
+ data.tar.gz: 8714538569c7c1211a93ffa567401fe54197ce975fb3946e1524550f67cacb97
5
5
  SHA512:
6
- metadata.gz: 2ac2c2933550c7a524d74f443e119d8c83c84cd1e3857c66a9ef57a4784694359e2004947bc8174ea3a28363fe247b145ef04034bc12bc31ed3a826f2be2d4a1
7
- data.tar.gz: a9ac55676eb5b6b75ffc2d74b146e1023783313c0e5aca96159f9845a814dd184427e958102b6b7a173e41ab4f67b11532d72a7d104e64d7b767c97d4f1bd242
6
+ metadata.gz: a072c85e99b507dc4cdf65ec04cb9ed1d83e3ef22ef6ba77bd2866b7e04d947920cd607e087be3c93b06d362ef2e94127903e5087b8e9b7a85c456be72684b6f
7
+ data.tar.gz: 481b0308e707c86fddc515ba26a079bb23c31bb1f0f0f42da00cc7d798af52a24f6f8ea40f75356675f856f037b4e7dafa4174e1d62589569379bdc36e26f401
@@ -269,6 +269,45 @@ module Brainiac
269
269
  def self.completions
270
270
  %w[setup config default map owner token status]
271
271
  end
272
+
273
+ # Called by brainiac CLI after `agent create` — prompts for Discord user ID.
274
+ def self.on_agent_created(agent_key, entry)
275
+ return unless $stdin.tty?
276
+
277
+ display_name = entry["display_name"] || agent_key.capitalize
278
+ puts ""
279
+ puts " [Discord] Configure Discord for #{display_name}?"
280
+ print " Discord user ID (or blank to skip): "
281
+ user_id = $stdin.gets&.chomp
282
+ return if user_id.nil? || user_id.empty?
283
+
284
+ config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "discord.json")
285
+ config = File.exist?(config_file) ? JSON.parse(File.read(config_file)) : {}
286
+ config["user_mappings"] ||= {}
287
+ config["user_mappings"][display_name] = user_id
288
+ File.write(config_file, JSON.pretty_generate(config))
289
+ puts " ✓ Added #{display_name} → #{user_id} to discord.json user_mappings"
290
+ rescue JSON::ParserError => e
291
+ puts " ⚠ Could not update discord.json: #{e.message}"
292
+ end
293
+
294
+ # Called by brainiac CLI after `agent remove` — removes from Discord user_mappings.
295
+ def self.on_agent_removed(agent_key, display_name)
296
+ config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "discord.json")
297
+ return unless File.exist?(config_file)
298
+
299
+ config = JSON.parse(File.read(config_file))
300
+ mappings = config["user_mappings"]
301
+ return unless mappings
302
+
303
+ removed = mappings.delete(display_name) || mappings.delete(agent_key)
304
+ return unless removed
305
+
306
+ File.write(config_file, JSON.pretty_generate(config))
307
+ puts " [Discord] Removed #{display_name} from discord.json user_mappings"
308
+ rescue JSON::ParserError
309
+ nil
310
+ end
272
311
  end
273
312
  end
274
313
  end
@@ -79,6 +79,27 @@ module Brainiac
79
79
  end
80
80
  end
81
81
 
82
+ # Start a single bot gateway connection dynamically (e.g. after agent is added).
83
+ # No-op if the bot is already running.
84
+ def start_bot!(agent_key, token)
85
+ @bots_mutex.synchronize do
86
+ return if @bots[agent_key]
87
+
88
+ @bots[agent_key] = { token: token, status: "starting", user_id: nil }
89
+ end
90
+ start_gateway_for(agent_key, token)
91
+ end
92
+
93
+ # Stop a bot gateway connection (e.g. after agent is removed).
94
+ # Removes it from the bots hash so it won't be matched in detect_sender_agent.
95
+ def stop_bot!(agent_key)
96
+ info = @bots_mutex.synchronize { @bots.delete(agent_key) }
97
+ return unless info
98
+
99
+ info[:thread]&.kill
100
+ LOG.info "[Discord] Gateway stopped for #{agent_key}" if defined?(LOG)
101
+ end
102
+
82
103
  # Summary of all bot statuses for the API endpoint.
83
104
  def bots_status
84
105
  @bots_mutex.synchronize do
@@ -122,7 +143,7 @@ module Brainiac
122
143
  private
123
144
 
124
145
  def start_gateway_for(agent_key, bot_token)
125
- Thread.new do
146
+ thread = Thread.new do
126
147
  agent_display = agent_display_name(agent_key) || agent_key.capitalize
127
148
  bot_user_id = nil
128
149
 
@@ -136,6 +157,7 @@ module Brainiac
136
157
  sleep 5
137
158
  end
138
159
  end
160
+ @bots_mutex.synchronize { @bots[agent_key][:thread] = thread if @bots[agent_key] }
139
161
  end
140
162
 
141
163
  def run_gateway_connection(agent_key, agent_display, bot_token, bot_user_id)
@@ -31,7 +31,10 @@ module Brainiac
31
31
  channel_info, is_thread, is_dm, in_own_thread = detect_channel_context(message, channel_id, mentioned, is_reply_to_bot, bot_token,
32
32
  bot_user_id)
33
33
  return if should_stand_down?(in_own_thread, mentioned, is_reply_to_bot, is_bot, agent_key, mentions, content)
34
- return unless mentioned || in_own_thread || is_dm || is_reply_to_bot
34
+
35
+ is_thread_participant = !mentioned && !in_own_thread && is_thread &&
36
+ thread_participant?(channel_id, message_id, bot_user_id, bot_token)
37
+ return unless mentioned || in_own_thread || is_dm || is_reply_to_bot || is_thread_participant
35
38
 
36
39
  record_human_comment("discord-#{channel_id}") unless is_bot
37
40
 
@@ -158,6 +161,24 @@ module Brainiac
158
161
  other_bot_mentioned
159
162
  end
160
163
 
164
+ # Check if this bot was previously mentioned in the thread (making it a participant).
165
+ # Uses a small history fetch to avoid excessive API calls.
166
+ def thread_participant?(channel_id, before_message_id, bot_user_id, bot_token)
167
+ messages = Api.request(:get, "/channels/#{channel_id}/messages?before=#{before_message_id}&limit=50", token: bot_token)
168
+ return false unless messages.is_a?(Array)
169
+
170
+ bot_id_str = bot_user_id.to_s
171
+ messages.any? do |msg|
172
+ mentions = msg["mentions"] || []
173
+ mentions.any? { |m| m["id"].to_s == bot_id_str } ||
174
+ (msg["content"] || "").match?(/<@!?#{Regexp.escape(bot_id_str)}>/) ||
175
+ msg.dig("author", "id").to_s == bot_id_str
176
+ end
177
+ rescue StandardError => e
178
+ LOG.warn "[Discord] Failed to check thread participation: #{e.message}" if defined?(LOG)
179
+ false
180
+ end
181
+
161
182
  def prepare_content(content, bot_user_id)
162
183
  content.gsub(/<@!?#{bot_user_id}>/, "").strip
163
184
  end
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Discord
6
- VERSION = "0.0.3"
6
+ VERSION = "0.0.4"
7
7
  end
8
8
  end
9
9
  end
@@ -31,6 +31,9 @@ module Brainiac
31
31
  # Register crash handler
32
32
  register_crash_handler!
33
33
 
34
+ # Register agent lifecycle hooks
35
+ register_agent_lifecycle_hooks!
36
+
34
37
  # Start all per-agent Discord bot gateway connections
35
38
  Brainiac::Plugins::Discord::Gateway.start_all!
36
39
 
@@ -45,6 +48,30 @@ module Brainiac
45
48
 
46
49
  private
47
50
 
51
+ def register_agent_lifecycle_hooks!
52
+ Brainiac.on(:agent_added) do |ctx|
53
+ agent_key = ctx[:agent_key]
54
+ entry = ctx[:entry]
55
+ display_name = ctx[:display_name]
56
+ next unless entry.is_a?(Hash)
57
+
58
+ token = (entry["env"] || {})["DISCORD_BOT_TOKEN"]
59
+ if token
60
+ # Start a gateway connection for the new agent's bot
61
+ Gateway.start_bot!(agent_key, token)
62
+ LOG.info "[Discord] Started bot for new agent: #{display_name}" if defined?(LOG)
63
+ end
64
+ end
65
+
66
+ Brainiac.on(:agent_removed) do |ctx|
67
+ agent_key = ctx[:agent_key]
68
+
69
+ # Stop the gateway connection if running
70
+ Gateway.stop_bot!(agent_key)
71
+ LOG.info "[Discord] Stopped bot for removed agent: #{agent_key}" if defined?(LOG)
72
+ end
73
+ end
74
+
48
75
  def register_notification_handler!
49
76
  Brainiac.on(:notify) do |ctx|
50
77
  next unless ctx[:channel].to_s == "discord"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-discord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis