brainiac-discord 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.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lightweight metadata for brainiac-discord.
4
+ # Loaded by `brainiac help` without pulling in the full plugin runtime.
5
+
6
+ require_relative "version"
7
+
8
+ module Brainiac
9
+ module Plugins
10
+ module Discord
11
+ # Returns true if Discord has at least one bot token configured.
12
+ def self.configured?
13
+ registry_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "agents.json")
14
+ return false unless File.exist?(registry_file)
15
+
16
+ registry = JSON.parse(File.read(registry_file))
17
+ registry.any? { |_k, v| v.is_a?(Hash) && v.dig("env", "DISCORD_BOT_TOKEN") }
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ # Help text shown in `brainiac help` when the plugin is installed.
23
+ def self.help_text
24
+ " brainiac discord <command> Manage Discord bots (config, token, status, map)"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brainiac
4
+ module Plugins
5
+ module Discord
6
+ # Discord prompt templates.
7
+ #
8
+ # CHANNEL — Discord-specific rules prepended to every Discord session.
9
+ # SITUATION — The standard Discord dispatch template with conversation context.
10
+ module Prompts
11
+ CHANNEL = <<~PROMPT
12
+ ## Discord Channel Rules
13
+
14
+ ### Mentions
15
+ Discord does NOT support plain-text @mentions. Writing `@Galen` renders as plain text.
16
+ To actually mention someone, use the `<@USER_ID>` format. Here are the known IDs:
17
+ {{DISCORD_MENTION_ROSTER}}
18
+
19
+ If you need to mention someone not on this list, just write their name without the @ symbol.
20
+ Do NOT @mention other agent bots unless the user explicitly asks you to bring them into the conversation.
21
+ Mentioning another agent triggers an automated dispatch — doing it casually can cause loops.
22
+
23
+ ### Formatting
24
+ Do NOT use HTML formatting. Use plain text or Discord markdown:
25
+ - ```code blocks``` for code
26
+ - **bold** for emphasis
27
+ - *italic* for softer emphasis
28
+ - > quotes for referencing
29
+
30
+ ### Response Delivery
31
+ You MUST write your response to a file at `{{RESPONSE_FILE}}`.
32
+ Do NOT respond via stdout — your response will only be delivered if written to this file.
33
+ Keep it conversational and concise — Discord messages have a 2000 char limit
34
+ per message, though long responses will be split automatically.
35
+
36
+ ### Scope
37
+ This is a conversational interaction — no card, no PR. You're here to answer questions,
38
+ discuss code, share knowledge, or help with whatever the user needs.
39
+
40
+ **Detect user intent:**
41
+ - If they're asking you to **implement, fix, build, update, or change** something → do the work
42
+ - If they're asking questions, discussing ideas, or seeking advice → respond conversationally
43
+
44
+ **When doing implementation work:**
45
+ 1. Create a worktree branching from `origin/main` (or the default branch shown in Project Context):
46
+ `git worktree add -b discord-<topic>-<timestamp> ../<repo>--discord-<topic>-<timestamp> origin/main`
47
+ 2. `cd` into the new worktree directory
48
+ 3. Make the changes, test if applicable
49
+ 4. Commit with a clear message
50
+ 5. Push the branch
51
+ 6. Summarize what you did in your response file
52
+ 7. If it's substantial or needs review, mention opening a PR (but don't create it unless asked)
53
+
54
+ **When responding conversationally:**
55
+ - Answer questions about the codebase, architecture, conventions
56
+ - Search your brain (knowledge + persona) for relevant context
57
+ - Read files from registered project repos to investigate questions
58
+ - Update your knowledge or persona files if the conversation warrants it
59
+
60
+ ### GIFs (optional)
61
+ You can optionally include a GIF in your Discord response to add personality.
62
+ To find one, search the local GIF API:
63
+ ```
64
+ curl -s "http://localhost:4567/api/gif?q=your+search+terms"
65
+ ```
66
+ This returns JSON with a `results` array. Each result has a `url` field — paste that
67
+ URL on its own line in your response and Discord will auto-embed it as an animated GIF.
68
+
69
+ **Guidelines:**
70
+ - GIFs should be RARE — include one in roughly 15% of responses, not more
71
+ - Default to NO GIF. Only include one when the moment is a genuine zinger — a perfectly landed joke, a dramatic reveal, a celebration that demands visual punctuation, or a response so good it needs the exclamation point of a GIF
72
+ - Skip GIFs for routine answers, technical implementation work, status updates, or when the tone doesn't call for one
73
+ - Match the GIF to the emotional tone — celebration, sarcasm, emphasis, humor
74
+ - Surprise is good — pick GIFs that are unexpected or perfectly timed, not generic
75
+ - Pick the most relevant result, not just the first one
76
+ - If the API returns no results or errors, just skip the GIF — don't mention it
77
+
78
+ ### Thread Memory (CRITICAL for long conversations)
79
+ Discord threads drift — your context window only shows recent messages, not the full history.
80
+ When writing your memory file for a Discord thread session, you MUST include:
81
+ - The original question/topic that started the thread (from "Original Message" above or your prior memory)
82
+ - A condensed summary of ALL topics discussed so far, not just this session
83
+ - Any topic shifts that occurred — what changed and why
84
+ - The current topic/focus as of this session
85
+ This is the ONLY way future sessions will know what happened in the middle of the conversation.
86
+
87
+ PROMPT
88
+
89
+ SITUATION = <<~'PROMPT'
90
+ ## Context
91
+
92
+ **From:** {{DISCORD_USER}} in #{{CHANNEL_NAME}}
93
+ {{REPLY_CONTEXT}}**Message:**
94
+ {{MESSAGE_BODY}}
95
+
96
+ {{THREAD_ROOT_CONTEXT}}### Recent Channel History
97
+ These are the messages immediately before the one above, for conversational context:
98
+ ```
99
+ {{CHANNEL_HISTORY}}
100
+ ```
101
+
102
+ {{PROJECT_CONTEXT}}
103
+
104
+ **IMPORTANT: Write your response to `{{RESPONSE_FILE}}`. Do NOT reply via stdout.**
105
+ PROMPT
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brainiac
4
+ module Plugins
5
+ module Discord
6
+ # Discord reaction handler.
7
+ #
8
+ # Handles MESSAGE_REACTION_ADD events:
9
+ # - ❌ to cancel an active agent session
10
+ # - ❔/❓ to peek at the agent's thinking (last 10/20 lines)
11
+ # - 🧠 to stream the full thinking log to a thread
12
+ # - Non-reserved emojis logged as feedback to the agent's persona
13
+ module Reactions
14
+ class << self
15
+ def handle(reaction_data, agent_key, bot_token, bot_user_id)
16
+ channel_id = reaction_data["channel_id"]
17
+ message_id = reaction_data["message_id"]
18
+ user_id = reaction_data["user_id"]
19
+ emoji = reaction_data["emoji"]
20
+ emoji_name = emoji["name"]
21
+
22
+ agent_name = agent_display_name(agent_key) || agent_key.capitalize
23
+
24
+ # Ignore reactions from bots (including self)
25
+ return if user_id == bot_user_id
26
+
27
+ case emoji_name
28
+ when "❔", "❓"
29
+ handle_thinking_peek(agent_key, agent_name, channel_id, message_id, bot_token, line_count: emoji_name == "❔" ? 10 : 20)
30
+ when "🧠"
31
+ handle_thinking_stream(agent_key, agent_name, channel_id, message_id, bot_token)
32
+ when "❌"
33
+ handle_cancel(agent_key, agent_name, channel_id, message_id, bot_token)
34
+ else
35
+ unless Api::RESERVED_EMOJIS.include?(emoji_name)
36
+ Thread.new do
37
+ log_emoji_feedback(channel_id, message_id, user_id, emoji_name, agent_key, agent_name, bot_token)
38
+ rescue StandardError => e
39
+ LOG.warn "[Discord:#{agent_name}] Feedback logging failed: #{e.message}" if defined?(LOG)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Strip ANSI escape codes and non-ASCII from log output for Discord display.
48
+ def strip_ansi(text)
49
+ text.gsub(/\e\[[0-9;]*[a-zA-Z]/, "")
50
+ .gsub(/\x1b\[[0-9;]*[a-zA-Z]/, "")
51
+ .gsub(/\e\][0-9;]*.*?(\x07|\e\\)/, "")
52
+ .gsub(/\e[=>]/, "")
53
+ .gsub(/\[\?[0-9]+[lh]/, "")
54
+ .gsub("[K", "")
55
+ .encode("ASCII", invalid: :replace, undef: :replace, replace: "")
56
+ .strip
57
+ end
58
+
59
+ def handle_thinking_peek(agent_key, agent_name, channel_id, message_id, bot_token, line_count:)
60
+ session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
61
+
62
+ ACTIVE_SESSIONS_MUTEX.synchronize do
63
+ session_info = ACTIVE_SESSIONS[session_key]
64
+
65
+ unless session_info
66
+ LOG.info "[Discord:#{agent_name}] Thinking peek on #{message_id} but no active session found" if defined?(LOG)
67
+ return
68
+ end
69
+
70
+ log_file = session_info[:log_file]
71
+ unless log_file && File.exist?(log_file)
72
+ LOG.warn "[Discord:#{agent_name}] No log file found for session #{session_key}" if defined?(LOG)
73
+ Api.send_message(channel_id, "No thinking file found for this session.", token: bot_token, reply_to: message_id)
74
+ return
75
+ end
76
+
77
+ LOG.info "[Discord:#{agent_name}] Reading last #{line_count} lines from #{log_file}" if defined?(LOG)
78
+
79
+ lines = File.readlines(log_file).last(line_count)
80
+ thinking_output = strip_ansi(lines.join)
81
+
82
+ response = "**Last #{line_count} lines:**\n```\n#{thinking_output}\n```"
83
+ Api.send_message(channel_id, response, token: bot_token, reply_to: message_id)
84
+ end
85
+ end
86
+
87
+ def handle_thinking_stream(agent_key, agent_name, channel_id, message_id, bot_token)
88
+ session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
89
+
90
+ ACTIVE_SESSIONS_MUTEX.synchronize do
91
+ session_info = ACTIVE_SESSIONS[session_key]
92
+
93
+ unless session_info
94
+ LOG.info "[Discord:#{agent_name}] 🧠 reaction on #{message_id} but no active session found" if defined?(LOG)
95
+ return
96
+ end
97
+
98
+ log_file = session_info[:log_file]
99
+ unless log_file && File.exist?(log_file)
100
+ LOG.warn "[Discord:#{agent_name}] No log file found for session #{session_key}" if defined?(LOG)
101
+ Api.send_message(channel_id, "No thinking file found for this session.", token: bot_token, reply_to: message_id)
102
+ return
103
+ end
104
+
105
+ LOG.info "[Discord:#{agent_name}] Creating thread and streaming thinking from #{log_file}" if defined?(LOG)
106
+
107
+ thread_response = Api.create_thread(channel_id, message_id, name: "🧠 Thinking Stream", token: bot_token)
108
+ unless thread_response && thread_response["id"]
109
+ LOG.error "[Discord:#{agent_name}] Failed to create thread, response: #{thread_response.inspect}" if defined?(LOG)
110
+ return
111
+ end
112
+
113
+ thread_id = thread_response["id"]
114
+ stream_thinking_to_thread(log_file, thread_id, bot_token)
115
+ end
116
+ end
117
+
118
+ def stream_thinking_to_thread(log_file, thread_id, bot_token)
119
+ thinking_content = strip_ansi(File.read(log_file))
120
+
121
+ chunks = []
122
+ current_chunk = ""
123
+ thinking_content.lines.each do |line|
124
+ if current_chunk.length + line.length > 1900
125
+ chunks << current_chunk
126
+ current_chunk = line
127
+ else
128
+ current_chunk += line
129
+ end
130
+ end
131
+ chunks << current_chunk unless current_chunk.empty?
132
+
133
+ chunks.each do |chunk|
134
+ Api.send_message(thread_id, "```\n#{chunk}\n```", token: bot_token)
135
+ sleep 0.5
136
+ end
137
+ end
138
+
139
+ def handle_cancel(agent_key, agent_name, channel_id, message_id, bot_token)
140
+ session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
141
+
142
+ ACTIVE_SESSIONS_MUTEX.synchronize do
143
+ session_info = ACTIVE_SESSIONS[session_key]
144
+
145
+ unless session_info
146
+ LOG.info "[Discord:#{agent_name}] ❌ reaction on #{message_id} but no active session found" if defined?(LOG)
147
+ return
148
+ end
149
+
150
+ LOG.info "[Discord:#{agent_name}] Cancelling session for message #{message_id} (PID: #{session_info[:pid]})" if defined?(LOG)
151
+
152
+ begin
153
+ Process.kill("KILL", session_info[:pid])
154
+ LOG.info "[Discord:#{agent_name}] Killed agent process #{session_info[:pid]}" if defined?(LOG)
155
+ rescue Errno::ESRCH
156
+ LOG.warn "[Discord:#{agent_name}] Process #{session_info[:pid]} already exited" if defined?(LOG)
157
+ rescue Errno::EPERM
158
+ LOG.error "[Discord:#{agent_name}] Permission denied killing process #{session_info[:pid]}" if defined?(LOG)
159
+ end
160
+
161
+ ACTIVE_SESSIONS.delete(session_key)
162
+
163
+ begin
164
+ Api.remove_reaction(channel_id, message_id, "👀", token: bot_token)
165
+ Api.add_reaction(channel_id, message_id, "🛑", token: bot_token)
166
+ rescue StandardError => e
167
+ LOG.warn "[Discord:#{agent_name}] Failed to update reactions: #{e.message}" if defined?(LOG)
168
+ end
169
+
170
+ session_info[:draft_files]&.each { |file| FileUtils.rm_f(file) }
171
+ end
172
+ end
173
+
174
+ def log_emoji_feedback(channel_id, message_id, user_id, emoji_name, agent_key, agent_name, bot_token)
175
+ msg = Api.fetch_message(channel_id, message_id, token: bot_token, log_errors: false)
176
+ return unless msg&.dig("author", "bot")
177
+
178
+ bot_uid = Gateway.bot_user_id(agent_key)
179
+ return unless bot_uid && msg.dig("author", "id") == bot_uid
180
+
181
+ reactor = respond_to?(:find_user_by_discord_id) ? find_user_by_discord_id(user_id) : nil
182
+ reactor_name = reactor ? reactor["canonical_name"] : user_id
183
+
184
+ snippet = (msg["content"] || "")[0, 80].tr("\n", " ").strip
185
+ snippet = "#{snippet}..." if (msg["content"] || "").length > 80
186
+
187
+ feedback_dir = File.join(persona_dir_for(agent_name), "people")
188
+ FileUtils.mkdir_p(feedback_dir)
189
+ feedback_file = File.join(feedback_dir, "#{reactor_name.downcase.gsub(/[^a-z0-9]/, "-")}-feedback.md")
190
+
191
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M")
192
+ entry = "- #{timestamp} #{emoji_name} on: \"#{snippet}\" (channel: #{channel_id})\n"
193
+
194
+ if File.exist?(feedback_file)
195
+ File.open(feedback_file, "a") { |f| f.write(entry) }
196
+ else
197
+ File.write(feedback_file, "# Feedback from #{reactor_name}\n\n## Reaction Log\n#{entry}")
198
+ end
199
+
200
+ LOG.info "[Discord:#{agent_name}] Logged #{emoji_name} feedback from #{reactor_name} on message #{message_id}" if defined?(LOG)
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brainiac
4
+ module Plugins
5
+ module Discord
6
+ VERSION = "0.0.1"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "discord/version"
4
+ require_relative "discord/metadata"
5
+ require_relative "discord/cli"
6
+ require_relative "discord/config"
7
+ require_relative "discord/prompts"
8
+ require_relative "discord/api"
9
+ require_relative "discord/delivery"
10
+ require_relative "discord/reactions"
11
+ require_relative "discord/message"
12
+ require_relative "discord/gateway"
13
+
14
+ module Brainiac
15
+ module Plugins
16
+ module Discord
17
+ class << self
18
+ # Called by Brainiac plugin system during server startup.
19
+ #
20
+ # @param app [Sinatra::Application] The running Brainiac server
21
+ def register(app)
22
+ # Load Discord config
23
+ Brainiac::Plugins::Discord::Config.load!
24
+
25
+ # Register channel prompt
26
+ Brainiac.register_channel_prompt(:discord, Brainiac::Plugins::Discord::Prompts::CHANNEL)
27
+
28
+ # Register as notification provider
29
+ register_notification_handler!
30
+
31
+ # Register crash handler
32
+ register_crash_handler!
33
+
34
+ # Start all per-agent Discord bot gateway connections
35
+ Brainiac::Plugins::Discord::Gateway.start_all!
36
+
37
+ # Start draft delivery poller
38
+ Brainiac::Plugins::Discord::Delivery.start_poller!
39
+
40
+ # Set up API routes
41
+ setup_routes(app)
42
+
43
+ LOG.info "[Discord] Plugin registered (#{Brainiac::Plugins::Discord::Gateway.bot_count} bots)"
44
+ end
45
+
46
+ private
47
+
48
+ def register_notification_handler!
49
+ Brainiac.on(:notify) do |ctx|
50
+ next unless ctx[:channel].to_s == "discord"
51
+
52
+ target = ctx[:target]
53
+ message = ctx[:message]
54
+ agent = ctx[:agent]
55
+
56
+ agent_key = agent&.downcase&.gsub(/[^a-z0-9-]/, "-")
57
+ token = Gateway.bot_token(agent_key) ||
58
+ Gateway.discord_bot_tokens[agent_key] ||
59
+ Gateway.discord_bot_tokens.values.first
60
+ next unless token && target
61
+
62
+ # Handle forum posts
63
+ if ctx[:forum_title] && Api.forum_channel?(target, token: token)
64
+ Api.create_forum_post(target, title: ctx[:forum_title], content: message, token: token)
65
+ elsif ctx[:forum_reply_to_latest] && Api.forum_channel?(target, token: token)
66
+ latest = Api.find_latest_forum_thread(target, token: token)
67
+ if latest
68
+ Api.send_long_message(latest["id"], message, token: token)
69
+ else
70
+ Api.send_long_message(target, message, token: token)
71
+ end
72
+ else
73
+ Api.send_long_message(target, message, token: token)
74
+ end
75
+
76
+ :discord # Signal that we handled it
77
+ end
78
+ end
79
+
80
+ def register_crash_handler!
81
+ Brainiac.on(:agent_crashed) do |ctx|
82
+ next unless %i[discord cron].include?(ctx[:source])
83
+
84
+ source_context = ctx[:source_context] || {}
85
+ channel_id = source_context[:channel_id] || source_context.dig(:job, :notify_target) || source_context.dig(:job, :discord_channel_id)
86
+ next unless channel_id
87
+
88
+ bot_token = source_context[:bot_token]
89
+ unless bot_token
90
+ agent_key = source_context.dig(:job, :agent)&.downcase&.gsub(/[^a-z0-9-]/, "-")
91
+ bot_token = Gateway.bot_token(agent_key) || Gateway.discord_bot_tokens.values.first
92
+ end
93
+ next unless bot_token
94
+
95
+ snippet = ctx[:snippet]
96
+ snippet_block = snippet ? "\n```\n#{snippet[-1500..]}\n```" : ""
97
+ message = "💥 **#{ctx[:agent_name]} crashed** (exit code #{ctx[:exit_status]})\nLog: `#{ctx[:log_file]}`#{snippet_block}"
98
+
99
+ Api.send_long_message(channel_id, message, token: bot_token, reply_to: source_context[:message_id])
100
+ :discord
101
+ end
102
+ end
103
+
104
+ def setup_routes(app)
105
+ app.get "/api/discord" do
106
+ content_type :json
107
+ Brainiac::Plugins::Discord::Gateway.bots_status.to_json
108
+ end
109
+
110
+ app.get "/api/gif" do
111
+ content_type :json
112
+ query = params["q"]
113
+ halt 400, { error: "missing q param" }.to_json unless query && !query.empty?
114
+
115
+ results = Brainiac::Plugins::Discord::Api.search_gif(query)
116
+ { results: results }.to_json
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Brainiac Discord Plugin — entry point loaded by RubyGems.
4
+ require_relative "brainiac/plugins/discord"
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brainiac-discord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andy Davis
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: brainiac
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.0.9
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.9
26
+ - !ruby/object:Gem::Dependency
27
+ name: websocket-client-simple
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.8'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.25'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.25'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.75'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.75'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop-performance
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.25'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.25'
96
+ description: Full Discord integration for Brainiac — per-agent bot gateway connections,
97
+ message handling, session supersede, draft delivery, reaction handlers (cancel,
98
+ thinking peek, feedback logging), worktree management, and GIF support. Uses Brainiac's
99
+ hook system for lifecycle integration.
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - README.md
105
+ - lib/brainiac/plugins/discord.rb
106
+ - lib/brainiac/plugins/discord/api.rb
107
+ - lib/brainiac/plugins/discord/cli.rb
108
+ - lib/brainiac/plugins/discord/config.rb
109
+ - lib/brainiac/plugins/discord/delivery.rb
110
+ - lib/brainiac/plugins/discord/gateway.rb
111
+ - lib/brainiac/plugins/discord/message.rb
112
+ - lib/brainiac/plugins/discord/metadata.rb
113
+ - lib/brainiac/plugins/discord/prompts.rb
114
+ - lib/brainiac/plugins/discord/reactions.rb
115
+ - lib/brainiac/plugins/discord/version.rb
116
+ - lib/brainiac_discord.rb
117
+ homepage: https://github.com/stowzilla/brainiac-discord
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ rubygems_mfa_required: 'true'
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '3.4'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.6.9
137
+ specification_version: 4
138
+ summary: Discord bot plugin for Brainiac
139
+ test_files: []