brainiac 0.0.9 → 0.0.10

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.
@@ -1,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Discord reaction handler.
4
- #
5
- # Handles MESSAGE_REACTION_ADD events:
6
- # - ❌ to cancel an active agent session
7
- # - ❔/❓ to peek at the agent's thinking (last 10/20 lines)
8
- # - 🧠 to stream the full thinking log to a thread
9
- # - Non-reserved emojis logged as feedback to the agent's persona
10
-
11
- # Strip ANSI escape codes and non-ASCII from log output for Discord display.
12
- def strip_ansi(text)
13
- text.gsub(/\e\[[0-9;]*[a-zA-Z]/, "")
14
- .gsub(/\x1b\[[0-9;]*[a-zA-Z]/, "")
15
- .gsub(/\e\][0-9;]*.*?(\x07|\e\\)/, "")
16
- .gsub(/\e[=>]/, "")
17
- .gsub(/\[\?[0-9]+[lh]/, "")
18
- .gsub("[K", "")
19
- .encode("ASCII", invalid: :replace, undef: :replace, replace: "")
20
- .strip
21
- end
22
-
23
- def handle_discord_reaction(reaction_data, agent_key, bot_token, bot_user_id)
24
- channel_id = reaction_data["channel_id"]
25
- message_id = reaction_data["message_id"]
26
- user_id = reaction_data["user_id"]
27
- emoji = reaction_data["emoji"]
28
- emoji_name = emoji["name"]
29
-
30
- agent_name = agent_display_name(agent_key) || agent_key.capitalize
31
-
32
- # Ignore reactions from bots (including self)
33
- return if user_id == bot_user_id
34
-
35
- # Handle ❔ or ❓ reactions (thinking file inspection)
36
- if ["❔", "❓"].include?(emoji_name)
37
- handle_thinking_peek(agent_key, agent_name, channel_id, message_id, bot_token, line_count: emoji_name == "❔" ? 10 : 20)
38
- return
39
- end
40
-
41
- # Handle 🧠 reaction (stream full thinking to thread)
42
- if emoji_name == "🧠"
43
- handle_thinking_stream(agent_key, agent_name, channel_id, message_id, bot_token)
44
- return
45
- end
46
-
47
- # --- Feedback logging for non-reserved emojis ---
48
- unless RESERVED_EMOJIS.include?(emoji_name)
49
- Thread.new do
50
- log_emoji_feedback(channel_id, message_id, user_id, emoji_name, agent_key, agent_name, bot_token)
51
- rescue StandardError => e
52
- LOG.warn "[Discord:#{agent_name}] Feedback logging failed: #{e.message}"
53
- end
54
- return
55
- end
56
-
57
- # Only handle ❌ reactions beyond this point
58
- return unless emoji_name == "❌"
59
-
60
- handle_cancel_reaction(agent_key, agent_name, channel_id, message_id, bot_token)
61
- end
62
-
63
- # --- Thinking Peek (❔/❓) ---
64
-
65
- def handle_thinking_peek(agent_key, agent_name, channel_id, message_id, bot_token, line_count:)
66
- session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
67
-
68
- ACTIVE_SESSIONS_MUTEX.synchronize do
69
- session_info = ACTIVE_SESSIONS[session_key]
70
-
71
- unless session_info
72
- LOG.info "[Discord:#{agent_name}] Thinking peek on #{message_id} but no active session found"
73
- return
74
- end
75
-
76
- log_file = session_info[:log_file]
77
- unless log_file && File.exist?(log_file)
78
- LOG.warn "[Discord:#{agent_name}] No log file found for session #{session_key}"
79
- send_discord_message(channel_id, "No thinking file found for this session.", token: bot_token, reply_to: message_id)
80
- return
81
- end
82
-
83
- LOG.info "[Discord:#{agent_name}] Reading last #{line_count} lines from #{log_file}"
84
-
85
- lines = File.readlines(log_file).last(line_count)
86
- thinking_output = strip_ansi(lines.join)
87
-
88
- response = "**Last #{line_count} lines:**\n```\n#{thinking_output}\n```"
89
- send_discord_message(channel_id, response, token: bot_token, reply_to: message_id)
90
- end
91
- end
92
-
93
- # --- Thinking Stream (🧠) ---
94
-
95
- def handle_thinking_stream(agent_key, agent_name, channel_id, message_id, bot_token)
96
- session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
97
-
98
- ACTIVE_SESSIONS_MUTEX.synchronize do
99
- session_info = ACTIVE_SESSIONS[session_key]
100
-
101
- unless session_info
102
- LOG.info "[Discord:#{agent_name}] 🧠 reaction on #{message_id} but no active session found"
103
- return
104
- end
105
-
106
- log_file = session_info[:log_file]
107
- unless log_file && File.exist?(log_file)
108
- LOG.warn "[Discord:#{agent_name}] No log file found for session #{session_key}"
109
- send_discord_message(channel_id, "No thinking file found for this session.", token: bot_token, reply_to: message_id)
110
- return
111
- end
112
-
113
- LOG.info "[Discord:#{agent_name}] Creating thread and streaming thinking from #{log_file}"
114
-
115
- thread_response = create_discord_thread(channel_id, message_id, name: "🧠 Thinking Stream", token: bot_token)
116
- unless thread_response && thread_response["id"]
117
- LOG.error "[Discord:#{agent_name}] Failed to create thread, response: #{thread_response.inspect}"
118
- return
119
- end
120
-
121
- thread_id = thread_response["id"]
122
- LOG.info "[Discord:#{agent_name}] Thread created: #{thread_id}"
123
-
124
- stream_thinking_to_thread(log_file, thread_id, bot_token)
125
- end
126
- end
127
-
128
- def stream_thinking_to_thread(log_file, thread_id, bot_token)
129
- thinking_content = strip_ansi(File.read(log_file))
130
-
131
- chunks = []
132
- current_chunk = ""
133
- thinking_content.lines.each do |line|
134
- if current_chunk.length + line.length > 1900
135
- chunks << current_chunk
136
- current_chunk = line
137
- else
138
- current_chunk += line
139
- end
140
- end
141
- chunks << current_chunk unless current_chunk.empty?
142
-
143
- chunks.each do |chunk|
144
- send_discord_message(thread_id, "```\n#{chunk}\n```", token: bot_token)
145
- sleep 0.5
146
- end
147
- end
148
-
149
- # --- Cancel (❌) ---
150
-
151
- def handle_cancel_reaction(agent_key, agent_name, channel_id, message_id, bot_token)
152
- session_key = "discord-#{agent_key}-#{channel_id}-#{message_id}"
153
-
154
- ACTIVE_SESSIONS_MUTEX.synchronize do
155
- session_info = ACTIVE_SESSIONS[session_key]
156
-
157
- unless session_info
158
- LOG.info "[Discord:#{agent_name}] ❌ reaction on #{message_id} but no active session found"
159
- return
160
- end
161
-
162
- LOG.info "[Discord:#{agent_name}] Cancelling session for message #{message_id} (PID: #{session_info[:pid]})"
163
-
164
- begin
165
- Process.kill("KILL", session_info[:pid])
166
- LOG.info "[Discord:#{agent_name}] Killed agent process #{session_info[:pid]}"
167
- rescue Errno::ESRCH
168
- LOG.warn "[Discord:#{agent_name}] Process #{session_info[:pid]} already exited"
169
- rescue Errno::EPERM
170
- LOG.error "[Discord:#{agent_name}] Permission denied killing process #{session_info[:pid]}"
171
- end
172
-
173
- ACTIVE_SESSIONS.delete(session_key)
174
-
175
- begin
176
- remove_discord_reaction(channel_id, message_id, "👀", token: bot_token)
177
- add_discord_reaction(channel_id, message_id, "🛑", token: bot_token)
178
- rescue StandardError => e
179
- LOG.warn "[Discord:#{agent_name}] Failed to update reactions: #{e.message}"
180
- end
181
-
182
- session_info[:draft_files]&.each { |file| FileUtils.rm_f(file) }
183
- end
184
- end
185
-
186
- # --- Emoji Feedback Logging ---
187
-
188
- def log_emoji_feedback(channel_id, message_id, user_id, emoji_name, agent_key, agent_name, bot_token)
189
- msg = fetch_discord_message(channel_id, message_id, token: bot_token, log_errors: false)
190
- return unless msg&.dig("author", "bot")
191
-
192
- bot_user_id = DISCORD_BOTS_MUTEX.synchronize { DISCORD_BOTS.dig(agent_key, :user_id) }
193
- return unless bot_user_id && msg.dig("author", "id") == bot_user_id
194
-
195
- reactor = find_user_by_discord_id(user_id)
196
- reactor_name = reactor ? reactor["canonical_name"] : user_id
197
-
198
- snippet = (msg["content"] || "")[0, 80].tr("\n", " ").strip
199
- snippet = "#{snippet}..." if (msg["content"] || "").length > 80
200
-
201
- feedback_dir = File.join(persona_dir_for(agent_name), "people")
202
- FileUtils.mkdir_p(feedback_dir)
203
- feedback_file = File.join(feedback_dir, "#{reactor_name.downcase.gsub(/[^a-z0-9]/, "-")}-feedback.md")
204
-
205
- timestamp = Time.now.strftime("%Y-%m-%d %H:%M")
206
- entry = "- #{timestamp} #{emoji_name} on: \"#{snippet}\" (channel: #{channel_id})\n"
207
-
208
- if File.exist?(feedback_file)
209
- File.open(feedback_file, "a") { |f| f.write(entry) }
210
- else
211
- File.write(feedback_file, "# Feedback from #{reactor_name}\n\n## Reaction Log\n#{entry}")
212
- end
213
-
214
- LOG.info "[Discord:#{agent_name}] Logged #{emoji_name} feedback from #{reactor_name} on message #{message_id}"
215
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Discord bot handlers: per-agent gateway connections, message handling, API helpers.
4
- #
5
- # Each agent with a `discord_bot_token` in the agent registry gets its own
6
- # Discord bot connection. Users @mention @Galen or @GLaDOS directly in Discord
7
- # rather than a single shared bot.
8
- #
9
- # This file loads all Discord sub-modules:
10
- # discord/config.rb — Config loading, thread map, channel routing
11
- # discord/api.rb — REST API helpers (also used by GitHub/Zoho for notifications)
12
- # discord/delivery.rb — Draft file delivery, poller, shared thread management
13
- # discord/reactions.rb — Reaction handler (cancel, thinking peek, feedback)
14
- # discord/message.rb — Main message handler and agent dispatch
15
- # discord/gateway.rb — WebSocket gateway connections per agent bot
16
-
17
- require "English"
18
-
19
- require_relative "discord/config"
20
- require_relative "discord/api"
21
- require_relative "discord/delivery"
22
- require_relative "discord/reactions"
23
- require_relative "discord/message"
24
- require_relative "discord/gateway"
@@ -1,17 +0,0 @@
1
- {
2
- "default_project": "marketplace",
3
- "owner_discord_id": "DISCORD_USER_ID",
4
- "dashboard_token": null,
5
- "channel_mappings": {
6
- "OPTIONAL_CHANNEL_ID": {
7
- "project": "brainiac"
8
- }
9
- },
10
- "user_mappings": {
11
- "Andy": "DISCORD_USER_ID",
12
- "Adam": "DISCORD_USER_ID",
13
- "Kaylee": "DISCORD_BOT_USER_ID_FROM_OTHER_MACHINE"
14
- },
15
- "authorized_role_ids": [],
16
- "authorized_user_ids": []
17
- }