brainiac 0.0.9 → 0.0.11
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/Gemfile.lock +2 -11
- data/bin/brainiac +1039 -316
- data/bin/brainiac-completion.bash +93 -18
- data/brainiac.gemspec +2 -2
- data/lib/brainiac/config.rb +9 -21
- data/lib/brainiac/cron.rb +41 -26
- data/lib/brainiac/handlers/github.rb +3 -28
- data/lib/brainiac/handlers/zoho.rb +25 -33
- data/lib/brainiac/helpers.rb +6 -16
- data/lib/brainiac/notifications.rb +96 -0
- data/lib/brainiac/prompts.rb +2 -102
- data/lib/brainiac/restart.rb +2 -10
- data/lib/brainiac/routes/api.rb +2 -14
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +9 -60
- data/skills/brainiac-plugins/SKILL.md +220 -0
- metadata +5 -25
- data/lib/brainiac/handlers/discord/api.rb +0 -196
- data/lib/brainiac/handlers/discord/config.rb +0 -134
- data/lib/brainiac/handlers/discord/delivery.rb +0 -196
- data/lib/brainiac/handlers/discord/gateway.rb +0 -212
- data/lib/brainiac/handlers/discord/message.rb +0 -921
- data/lib/brainiac/handlers/discord/reactions.rb +0 -215
- data/lib/brainiac/handlers/discord.rb +0 -24
- data/templates/discord.json.example +0 -17
|
@@ -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
|
-
}
|