brainiac-discord 0.0.3 → 0.0.5

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: 5edd0068ecc82d20372d269791ff8e750fcec8106f4adcc99fdef3c252e03ada
4
+ data.tar.gz: b30c999fc3497d7608463975a3d9f4a8c09b135e3d7bc9da1099b6a5306594c2
5
5
  SHA512:
6
- metadata.gz: 2ac2c2933550c7a524d74f443e119d8c83c84cd1e3857c66a9ef57a4784694359e2004947bc8174ea3a28363fe247b145ef04034bc12bc31ed3a826f2be2d4a1
7
- data.tar.gz: a9ac55676eb5b6b75ffc2d74b146e1023783313c0e5aca96159f9845a814dd184427e958102b6b7a173e41ab4f67b11532d72a7d104e64d7b767c97d4f1bd242
6
+ metadata.gz: f65f1deefed415c9b3a038162ad6c59714f2677c9ce8ec201e880ce9ae9b159571cb4910a7515268d0e09ad262e69011ea6f7b1c40471bc846ea5f84fa74d366
7
+ data.tar.gz: 23ba4bda9f6eb4e9797e6724ad57f12908da3176f116eacdaa11f8b5c3462647180cb8c0a1a1766c00ac12de49c2216e8fdf84da9ad719dcde0bae42307851f9
@@ -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
@@ -75,6 +75,18 @@ module Brainiac
75
75
  end.map(&:to_s)
76
76
  end
77
77
 
78
+ # Find the Discord role ID for a given agent key.
79
+ # Checks role_mappings and authorized_role_ids (when Hash) by agent display name.
80
+ # Returns the role ID string, or nil if not found.
81
+ def role_id_for_agent(agent_key)
82
+ roles = @config["role_mappings"] || (@config["authorized_role_ids"].is_a?(Hash) ? @config["authorized_role_ids"] : nil)
83
+ return nil unless roles
84
+
85
+ # Try capitalized key first (e.g., "galen" → "Galen"), then display name from registry
86
+ agent_name = agent_display_name(agent_key) || agent_key.capitalize
87
+ roles[agent_name]&.to_s
88
+ end
89
+
78
90
  def authorized_user_ids
79
91
  @config["authorized_user_ids"] || []
80
92
  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)
@@ -273,6 +295,26 @@ module Brainiac
273
295
  rescue StandardError => e
274
296
  LOG.error "[Discord:#{agent_display}] Error handling reaction: #{e.message}\n#{e.backtrace.first(3).join("\n")}" if defined?(LOG)
275
297
  end
298
+ when "THREAD_UPDATE"
299
+ if data.dig("thread_metadata", "archived")
300
+ Thread.new do
301
+ ThreadCleanup.handle_archive(data, agent_key, agent_display)
302
+ rescue StandardError => e
303
+ if defined?(LOG)
304
+ LOG.error "[Discord:#{agent_display}] Error handling thread archive: " \
305
+ "#{e.message}\n#{e.backtrace.first(3).join("\n")}"
306
+ end
307
+ end
308
+ end
309
+ when "THREAD_DELETE"
310
+ Thread.new do
311
+ ThreadCleanup.handle_archive(data, agent_key, agent_display)
312
+ rescue StandardError => e
313
+ if defined?(LOG)
314
+ LOG.error "[Discord:#{agent_display}] Error handling thread delete: " \
315
+ "#{e.message}\n#{e.backtrace.first(3).join("\n")}"
316
+ end
317
+ end
276
318
  end
277
319
 
278
320
  bot_user_id
@@ -281,6 +323,7 @@ module Brainiac
281
323
  def mark_bot_ready(agent_key, agent_display, bot_user_id, data)
282
324
  @bots_mutex.synchronize do
283
325
  @bots[agent_key][:user_id] = bot_user_id
326
+ @bots[agent_key][:username] = data.dig("user", "username")
284
327
  @bots[agent_key][:status] = "ready"
285
328
  end
286
329
  guild_count = data["guilds"]&.size || 0
@@ -24,14 +24,22 @@ module Brainiac
24
24
 
25
25
  mentions = message["mentions"] || []
26
26
  mentioned = mentions.any? { |m| m["id"].to_s == bot_user_id.to_s } ||
27
- content.match?(/<@!?#{Regexp.escape(bot_user_id.to_s)}>/)
27
+ content.match?(/<@!?#{Regexp.escape(bot_user_id.to_s)}>/) ||
28
+ role_mentioned?(message, content, agent_key)
28
29
  return if sender_agent_key && !validate_cross_agent_dispatch(sender_agent_key, agent_key, mentioned, content, channel_id)
29
30
 
30
31
  is_reply_to_bot, referenced_message = detect_reply_to_bot(message, channel_id, mentioned, bot_token, bot_user_id)
31
32
  channel_info, is_thread, is_dm, in_own_thread = detect_channel_context(message, channel_id, mentioned, is_reply_to_bot, bot_token,
32
33
  bot_user_id)
33
34
  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
35
+
36
+ is_thread_participant = !mentioned && !in_own_thread && is_thread &&
37
+ thread_participant?(channel_id, message_id, bot_user_id, bot_token)
38
+ return unless mentioned || in_own_thread || is_dm || is_reply_to_bot || is_thread_participant
39
+
40
+ # When another agent is explicitly mentioned, thread participants who weren't
41
+ # addressed should not activate — the message was directed at someone specific.
42
+ return if is_thread_participant && other_agent_mentioned?(mentions, content, agent_key)
35
43
 
36
44
  record_human_comment("discord-#{channel_id}") unless is_bot
37
45
 
@@ -59,6 +67,14 @@ module Brainiac
59
67
  tags = parse_inline_tags(clean_content)
60
68
  project_key, project_config = resolve_project(tags[:project], parent_channel_id, agent_name, channel_id, message_id, bot_token)
61
69
 
70
+ # Thread owners bypass intent checking ONLY when they're the sole agent in
71
+ # the thread (1-on-1 with the human). If other agents have participated,
72
+ # everyone goes through intent checking unless explicitly @mentioned.
73
+ solo_thread_owner = in_own_thread && solo_in_thread?(channel_history, agent_key)
74
+ if in_own_thread && defined?(LOG)
75
+ LOG.info "[Discord:#{agent_name}] Own thread — #{solo_thread_owner ? "solo (bypassing intent)" : "multi-agent (intent check applies)"}"
76
+ end
77
+
62
78
  route_dispatch(
63
79
  agent_key: agent_key, agent_name: agent_name, bot_token: bot_token, is_bot: is_bot,
64
80
  channel_id: channel_id, message_id: message_id, message: message,
@@ -67,12 +83,44 @@ module Brainiac
67
83
  channel_info: channel_info, parent_channel_id: parent_channel_id,
68
84
  discord_user: discord_user, reply_context: reply_context,
69
85
  channel_history: channel_history, project_key: project_key,
70
- project_config: project_config, attachment_paths: attachment_paths
86
+ project_config: project_config, attachment_paths: attachment_paths,
87
+ directly_addressed: mentioned || is_reply_to_bot || is_dm || solo_thread_owner
71
88
  )
72
89
  end
73
90
 
74
91
  private
75
92
 
93
+ # Check if the agent's Discord role was @mentioned in this message.
94
+ # Discord role mentions use <@&ROLE_ID> syntax and populate mention_roles array.
95
+ def role_mentioned?(message, content, agent_key)
96
+ role_id = Config.role_id_for_agent(agent_key)
97
+ return false unless role_id
98
+
99
+ mention_roles = message["mention_roles"] || []
100
+ mention_roles.any? { |r| r.to_s == role_id } ||
101
+ content.match?(/<@&#{Regexp.escape(role_id)}>/)
102
+ end
103
+
104
+ # Check if another agent bot is explicitly mentioned in this message.
105
+ # When a user @mentions a specific agent, thread participants who weren't
106
+ # mentioned should stay silent — no intent check needed.
107
+ def other_agent_mentioned?(mentions, content, agent_key)
108
+ mention_roles = []
109
+
110
+ Gateway.each_bot do |key, info|
111
+ next if key == agent_key
112
+ next unless info[:user_id]
113
+
114
+ return true if mentions.any? { |m| m["id"].to_s == info[:user_id].to_s } ||
115
+ content.match?(/<@!?#{Regexp.escape(info[:user_id].to_s)}>/)
116
+
117
+ role_id = Config.role_id_for_agent(key)
118
+ mention_roles << role_id if role_id
119
+ end
120
+
121
+ mention_roles.any? { |rid| content.match?(/<@&#{Regexp.escape(rid)}>/) }
122
+ end
123
+
76
124
  def validate_cross_agent_dispatch(sender_agent_key, agent_key, mentioned, content, channel_id)
77
125
  return false unless mentioned
78
126
 
@@ -158,6 +206,62 @@ module Brainiac
158
206
  other_bot_mentioned
159
207
  end
160
208
 
209
+ # Check if this bot was previously mentioned in the thread (making it a participant).
210
+ # Uses a small history fetch to avoid excessive API calls.
211
+ def thread_participant?(channel_id, before_message_id, bot_user_id, bot_token)
212
+ messages = Api.request(:get, "/channels/#{channel_id}/messages?before=#{before_message_id}&limit=50", token: bot_token)
213
+ return false unless messages.is_a?(Array)
214
+
215
+ bot_id_str = bot_user_id.to_s
216
+ messages.any? do |msg|
217
+ mentions = msg["mentions"] || []
218
+ mentions.any? { |m| m["id"].to_s == bot_id_str } ||
219
+ (msg["content"] || "").match?(/<@!?#{Regexp.escape(bot_id_str)}>/) ||
220
+ msg.dig("author", "id").to_s == bot_id_str
221
+ end
222
+ rescue StandardError => e
223
+ LOG.warn "[Discord] Failed to check thread participation: #{e.message}" if defined?(LOG)
224
+ false
225
+ end
226
+
227
+ # Check if this agent is the only bot in the thread (1-on-1 with the human).
228
+ # Uses the already-fetched channel_history to avoid extra API calls.
229
+ # channel_history is a formatted string ("Username: message\n...") from fetch_channel_history.
230
+ # Returns true when no other agent bots have posted in the thread history,
231
+ # meaning it's safe to skip intent checking (the message is obviously for us).
232
+ def solo_in_thread?(channel_history, agent_key)
233
+ return true unless channel_history.is_a?(String) && !channel_history.empty?
234
+
235
+ other_bot_usernames = []
236
+ Gateway.each_bot do |key, info|
237
+ next if key == agent_key
238
+ next unless info[:username]
239
+
240
+ other_bot_usernames << info[:username]
241
+ end
242
+
243
+ # Also check user_mappings for remote agent bots not connected locally
244
+ agent_keys = []
245
+ Gateway.each_bot { |key, _| agent_keys << key }
246
+ Config.user_mappings.each do |name, _discord_id|
247
+ normalized = name.downcase.gsub(/[^a-z0-9]/, "-")
248
+ next if normalized == agent_key
249
+ next if agent_keys.include?(normalized)
250
+
251
+ # Only include names that look like agent bots (present in agent registry)
252
+ next unless defined?(AGENT_REGISTRY) && AGENT_REGISTRY.key?(normalized)
253
+
254
+ other_bot_usernames << name
255
+ end
256
+
257
+ return true if other_bot_usernames.empty?
258
+
259
+ history_lines = channel_history.split("\n")
260
+ history_lines.none? do |line|
261
+ other_bot_usernames.any? { |username| line.start_with?("#{username}: ") }
262
+ end
263
+ end
264
+
161
265
  def prepare_content(content, bot_user_id)
162
266
  content.gsub(/<@!?#{bot_user_id}>/, "").strip
163
267
  end
@@ -276,7 +380,8 @@ module Brainiac
276
380
  def route_dispatch(agent_key:, agent_name:, bot_token:, is_bot:, channel_id:, message_id:, message:,
277
381
  clean_content:, clean_content_for_prompt:, chat_mode:, is_thread:, is_dm:,
278
382
  channel_info:, parent_channel_id:, discord_user:, reply_context:,
279
- channel_history:, project_key:, project_config:, attachment_paths:)
383
+ channel_history:, project_key:, project_config:, attachment_paths:,
384
+ directly_addressed: false)
280
385
  session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
281
386
  supersede_key = "discord-#{agent_key}-#{channel_id}"
282
387
  if session_active?(session_key)
@@ -285,9 +390,12 @@ module Brainiac
285
390
  end
286
391
  handle_supersede(is_bot, supersede_key, discord_user, agent_name, bot_token)
287
392
 
288
- if intent_skip?(clean_content, agent_name: agent_name, source: :discord, channel: "Discord #{is_thread ? "thread" : "channel"}")
289
- LOG.info "[Discord:#{agent_name}] Intent skip — not dispatching for: #{clean_content[0..80]}" if defined?(LOG)
290
- return
393
+ unless directly_addressed
394
+ if intent_skip?(clean_content, agent_name: agent_name, source: :discord,
395
+ channel: "Discord #{is_thread ? "thread" : "channel"}", context: channel_history)
396
+ LOG.info "[Discord:#{agent_name}] Intent skip — not dispatching for: #{clean_content[0..80]}" if defined?(LOG)
397
+ return
398
+ end
291
399
  end
292
400
 
293
401
  Thread.new do
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+
6
+ module Brainiac
7
+ module Plugins
8
+ module Discord
9
+ # Handles cleanup of Discord thread worktrees when threads are archived or deleted.
10
+ #
11
+ # When a thread is archived (manually closed or auto-archived due to inactivity)
12
+ # or deleted, this module looks up the associated worktree in discord_thread_map.json,
13
+ # verifies it has no uncommitted changes, removes the worktree + branch, and removes
14
+ # the thread map entry.
15
+ module ThreadCleanup
16
+ class << self
17
+ def handle_archive(data, agent_key, agent_display)
18
+ thread_id = data["id"]
19
+ return unless thread_id
20
+
21
+ thread_map_key = "#{agent_key}:#{thread_id}"
22
+
23
+ entry = Config.thread_map_mutex.synchronize do
24
+ map = Config.load_thread_map
25
+ map[thread_map_key]
26
+ end
27
+
28
+ unless entry
29
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — no worktree tracked, nothing to clean up" if defined?(LOG)
30
+ return
31
+ end
32
+
33
+ worktree_path = entry["worktree"]
34
+ branch = entry["branch"]
35
+ project_key = entry["project"]
36
+ chat_mode = entry["chat_mode"]
37
+
38
+ if chat_mode
39
+ cleanup_chat_mode_dir(worktree_path, thread_map_key, agent_display, thread_id)
40
+ else
41
+ cleanup_worktree(worktree_path, branch, project_key, thread_map_key, agent_display, thread_id)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def cleanup_chat_mode_dir(dir_path, thread_map_key, agent_display, thread_id)
48
+ if dir_path && File.directory?(dir_path)
49
+ FileUtils.rm_rf(dir_path)
50
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — removed chat mode dir #{dir_path}" if defined?(LOG)
51
+ elsif defined?(LOG)
52
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — chat mode dir already gone"
53
+ end
54
+
55
+ remove_thread_map_entry(thread_map_key)
56
+ end
57
+
58
+ def cleanup_worktree(worktree_path, branch, project_key, thread_map_key, agent_display, thread_id)
59
+ unless worktree_path && File.directory?(worktree_path)
60
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — worktree already removed, cleaning up map entry" if defined?(LOG)
61
+ remove_thread_map_entry(thread_map_key)
62
+ return
63
+ end
64
+
65
+ status_output, = Open3.capture3("git", "status", "--porcelain", chdir: worktree_path)
66
+ unless status_output.strip.empty?
67
+ if defined?(LOG)
68
+ LOG.warn "[Discord:#{agent_display}] Thread #{thread_id} archived — " \
69
+ "worktree #{worktree_path} has uncommitted changes, skipping cleanup"
70
+ end
71
+ return
72
+ end
73
+
74
+ project_config = defined?(PROJECTS) ? PROJECTS[project_key] : nil
75
+ repo_path = project_config&.dig("repo_path")
76
+
77
+ unless repo_path && File.directory?(repo_path)
78
+ if defined?(LOG)
79
+ LOG.warn "[Discord:#{agent_display}] Thread #{thread_id} archived — " \
80
+ "cannot find repo for project '#{project_key}', removing directory directly"
81
+ end
82
+ FileUtils.rm_rf(worktree_path)
83
+ remove_thread_map_entry(thread_map_key)
84
+ return
85
+ end
86
+
87
+ begin
88
+ Open3.capture3("git", "worktree", "remove", worktree_path, "--force", chdir: repo_path)
89
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — removed worktree #{worktree_path}" if defined?(LOG)
90
+ rescue StandardError => e
91
+ LOG.warn "[Discord:#{agent_display}] Failed to remove worktree #{worktree_path}: #{e.message}" if defined?(LOG)
92
+ end
93
+
94
+ if branch
95
+ begin
96
+ Open3.capture3("git", "branch", "-D", branch, chdir: repo_path)
97
+ LOG.info "[Discord:#{agent_display}] Thread #{thread_id} archived — deleted branch #{branch}" if defined?(LOG)
98
+ rescue StandardError => e
99
+ LOG.warn "[Discord:#{agent_display}] Failed to delete branch #{branch}: #{e.message}" if defined?(LOG)
100
+ end
101
+ end
102
+
103
+ remove_thread_map_entry(thread_map_key)
104
+ end
105
+
106
+ def remove_thread_map_entry(thread_map_key)
107
+ Config.thread_map_mutex.synchronize do
108
+ map = Config.load_thread_map
109
+ map.delete(thread_map_key)
110
+ Config.save_thread_map(map)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ 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.5"
7
7
  end
8
8
  end
9
9
  end
@@ -9,6 +9,7 @@ require_relative "discord/api"
9
9
  require_relative "discord/delivery"
10
10
  require_relative "discord/reactions"
11
11
  require_relative "discord/message"
12
+ require_relative "discord/thread_cleanup"
12
13
  require_relative "discord/gateway"
13
14
 
14
15
  module Brainiac
@@ -31,6 +32,9 @@ module Brainiac
31
32
  # Register crash handler
32
33
  register_crash_handler!
33
34
 
35
+ # Register agent lifecycle hooks
36
+ register_agent_lifecycle_hooks!
37
+
34
38
  # Start all per-agent Discord bot gateway connections
35
39
  Brainiac::Plugins::Discord::Gateway.start_all!
36
40
 
@@ -45,6 +49,30 @@ module Brainiac
45
49
 
46
50
  private
47
51
 
52
+ def register_agent_lifecycle_hooks!
53
+ Brainiac.on(:agent_added) do |ctx|
54
+ agent_key = ctx[:agent_key]
55
+ entry = ctx[:entry]
56
+ display_name = ctx[:display_name]
57
+ next unless entry.is_a?(Hash)
58
+
59
+ token = (entry["env"] || {})["DISCORD_BOT_TOKEN"]
60
+ if token
61
+ # Start a gateway connection for the new agent's bot
62
+ Gateway.start_bot!(agent_key, token)
63
+ LOG.info "[Discord] Started bot for new agent: #{display_name}" if defined?(LOG)
64
+ end
65
+ end
66
+
67
+ Brainiac.on(:agent_removed) do |ctx|
68
+ agent_key = ctx[:agent_key]
69
+
70
+ # Stop the gateway connection if running
71
+ Gateway.stop_bot!(agent_key)
72
+ LOG.info "[Discord] Stopped bot for removed agent: #{agent_key}" if defined?(LOG)
73
+ end
74
+ end
75
+
48
76
  def register_notification_handler!
49
77
  Brainiac.on(:notify) do |ctx|
50
78
  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.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -112,6 +112,7 @@ files:
112
112
  - lib/brainiac/plugins/discord/metadata.rb
113
113
  - lib/brainiac/plugins/discord/prompts.rb
114
114
  - lib/brainiac/plugins/discord/reactions.rb
115
+ - lib/brainiac/plugins/discord/thread_cleanup.rb
115
116
  - lib/brainiac/plugins/discord/version.rb
116
117
  - lib/brainiac_discord.rb
117
118
  homepage: https://github.com/stowzilla/brainiac-discord