brainiac-discord 0.0.1 → 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 +4 -4
- data/lib/brainiac/plugins/discord/cli.rb +39 -0
- data/lib/brainiac/plugins/discord/gateway.rb +34 -7
- data/lib/brainiac/plugins/discord/message.rb +30 -4
- data/lib/brainiac/plugins/discord/prompts.rb +27 -0
- data/lib/brainiac/plugins/discord/version.rb +1 -1
- data/lib/brainiac/plugins/discord.rb +27 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdd8beafe4070560e4caaaa465d2b1e9dde33defa1b7fdc429467864776d3c21
|
|
4
|
+
data.tar.gz: 8714538569c7c1211a93ffa567401fe54197ce975fb3946e1524550f67cacb97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,13 +157,19 @@ 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)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
# Capture module-level ivars in local variables so the event_emitter
|
|
165
|
+
# blocks (which run via instance_exec on the WS client) can access them.
|
|
166
|
+
bots = @bots
|
|
167
|
+
bots_mutex = @bots_mutex
|
|
168
|
+
|
|
169
|
+
bots_mutex.synchronize do
|
|
170
|
+
bots[agent_key] ||= {}
|
|
171
|
+
bots[agent_key][:status] = "connecting"
|
|
172
|
+
bots[agent_key][:token] = bot_token
|
|
146
173
|
end
|
|
147
174
|
|
|
148
175
|
LOG.debug "[Discord:#{agent_display}] Connecting to Gateway..." if defined?(LOG) && LOG.respond_to?(:debug)
|
|
@@ -168,8 +195,8 @@ module Brainiac
|
|
|
168
195
|
end
|
|
169
196
|
|
|
170
197
|
ws.on :close do |_e|
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
bots_mutex.synchronize do
|
|
199
|
+
bots[agent_key][:status] = "disconnected" if bots[agent_key]
|
|
173
200
|
end
|
|
174
201
|
LOG.warn "[Discord:#{agent_display}] WebSocket closed" if defined?(LOG)
|
|
175
202
|
heartbeat_thread&.kill
|
|
@@ -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
|
-
|
|
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
|
|
@@ -284,6 +305,12 @@ module Brainiac
|
|
|
284
305
|
return
|
|
285
306
|
end
|
|
286
307
|
handle_supersede(is_bot, supersede_key, discord_user, agent_name, bot_token)
|
|
308
|
+
|
|
309
|
+
if intent_skip?(clean_content, agent_name: agent_name, source: :discord, channel: "Discord #{is_thread ? "thread" : "channel"}")
|
|
310
|
+
LOG.info "[Discord:#{agent_name}] Intent skip — not dispatching for: #{clean_content[0..80]}" if defined?(LOG)
|
|
311
|
+
return
|
|
312
|
+
end
|
|
313
|
+
|
|
287
314
|
Thread.new do
|
|
288
315
|
Api.remove_reaction(channel_id, message_id, "🛑", token: bot_token)
|
|
289
316
|
Api.add_reaction(channel_id, message_id, "👀", token: bot_token)
|
|
@@ -567,8 +594,7 @@ module Brainiac
|
|
|
567
594
|
|
|
568
595
|
def thread_resume?(project_config, clean_content, thread_cli_provider, agent_name)
|
|
569
596
|
effective_provider = detect_cli_provider(text: clean_content) || thread_cli_provider
|
|
570
|
-
|
|
571
|
-
resolved["resume_flag"] ? true : false
|
|
597
|
+
resume_viable?(project_config: project_config, cli_provider: effective_provider, agent_name: agent_name)
|
|
572
598
|
end
|
|
573
599
|
|
|
574
600
|
def detect_thread_overrides(project_config, clean_content, fallback_cli: nil, fallback_model: nil, fallback_effort: nil)
|
|
@@ -630,7 +656,7 @@ module Brainiac
|
|
|
630
656
|
discord_user:, channel_name:, reply_context:, channel_history:, thread_root_context:,
|
|
631
657
|
project_context:, response_file:, card_id:, brain_context:, agent_name:)
|
|
632
658
|
if should_resume && thread_worktree_path
|
|
633
|
-
return
|
|
659
|
+
return Brainiac::Plugins::Discord::Prompts.render_resume(
|
|
634
660
|
message_body: clean_content_for_prompt, discord_user: discord_user,
|
|
635
661
|
response_file: response_file, agent_name: agent_name, card_id: card_id
|
|
636
662
|
)
|
|
@@ -103,6 +103,33 @@ module Brainiac
|
|
|
103
103
|
|
|
104
104
|
**IMPORTANT: Write your response to `{{RESPONSE_FILE}}`. Do NOT reply via stdout.**
|
|
105
105
|
PROMPT
|
|
106
|
+
|
|
107
|
+
# Lean resume prompt for Discord threads. The previous session has full context
|
|
108
|
+
# (role, persona, knowledge, instructions). We only send the new message.
|
|
109
|
+
def self.render_resume(message_body:, discord_user:, response_file:, agent_name: AI_AGENT_NAME, card_id: nil)
|
|
110
|
+
memory_dir = memory_dir_for(agent_name)
|
|
111
|
+
if card_id
|
|
112
|
+
memory_file = File.join(memory_dir, "card-#{card_id}.md")
|
|
113
|
+
FileUtils.mkdir_p(memory_dir)
|
|
114
|
+
FileUtils.touch(memory_file)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
lines = []
|
|
118
|
+
lines << "## Resumed Session — New Discord Message"
|
|
119
|
+
lines << ""
|
|
120
|
+
lines << "This is a continuation of your previous session in this thread."
|
|
121
|
+
lines << "All prior context, instructions, and your previous work are still in this conversation."
|
|
122
|
+
lines << ""
|
|
123
|
+
lines << "### New Message from #{discord_user}"
|
|
124
|
+
lines << ""
|
|
125
|
+
lines << message_body
|
|
126
|
+
lines << ""
|
|
127
|
+
lines << "---"
|
|
128
|
+
lines << "**IMPORTANT: Write your response to `#{response_file}`. Do NOT reply via stdout.**"
|
|
129
|
+
lines << "All your previous instructions still apply (memory, persona, one message per session, etc.)."
|
|
130
|
+
|
|
131
|
+
lines.join("\n")
|
|
132
|
+
end
|
|
106
133
|
end
|
|
107
134
|
end
|
|
108
135
|
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.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.14
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.14
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: websocket-client-simple
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|