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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -11
- data/bin/brainiac +946 -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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generic notification system for Brainiac.
|
|
4
|
+
#
|
|
5
|
+
# Provides a channel-agnostic way to send messages. Plugins register as
|
|
6
|
+
# notification providers for their channel. Core code emits notifications
|
|
7
|
+
# without knowing which plugin will deliver them.
|
|
8
|
+
#
|
|
9
|
+
# Configuration in ~/.brainiac/brainiac.json:
|
|
10
|
+
# "notifications": {
|
|
11
|
+
# "deploy": { "channel": "discord", "target": "channel-id-123" },
|
|
12
|
+
# "cron": { "channel": "discord", "target": "channel-id-456" },
|
|
13
|
+
# "restart": { "channel": "discord", "target": "channel-id-789" }
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
# Plugins register handlers:
|
|
17
|
+
# Brainiac.on(:notify) do |ctx|
|
|
18
|
+
# if ctx[:channel].to_s == "discord"
|
|
19
|
+
# send_to_discord(ctx[:target], ctx[:message], agent: ctx[:agent])
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
NOTIFICATIONS_CONFIG_KEY = "notifications"
|
|
24
|
+
|
|
25
|
+
# Send a notification via the configured channel.
|
|
26
|
+
#
|
|
27
|
+
# @param event [Symbol, String] The notification event type (e.g. :deploy, :restart, :cron)
|
|
28
|
+
# @param message [String] The message content
|
|
29
|
+
# @param target [String, nil] Override target (channel ID, card number, etc.)
|
|
30
|
+
# @param channel [Symbol, String, nil] Override channel (:discord, :fizzy, etc.)
|
|
31
|
+
# @param agent [String, nil] Which agent identity to send as
|
|
32
|
+
# @param metadata [Hash] Extra context passed to the handler
|
|
33
|
+
def send_notification(event, message, target: nil, channel: nil, agent: nil, **metadata)
|
|
34
|
+
config = notification_config_for(event)
|
|
35
|
+
|
|
36
|
+
# Explicit params override config
|
|
37
|
+
channel = (channel || config["channel"])&.to_sym
|
|
38
|
+
target ||= config["target"]
|
|
39
|
+
|
|
40
|
+
unless channel && target
|
|
41
|
+
LOG.debug "[Notify] No channel/target configured for '#{event}', skipping" if LOG.debug?
|
|
42
|
+
return false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
agent ||= config["agent"]
|
|
46
|
+
|
|
47
|
+
results = Brainiac.emit(:notify,
|
|
48
|
+
event: event.to_sym, channel: channel, target: target,
|
|
49
|
+
message: message, agent: agent, **metadata)
|
|
50
|
+
|
|
51
|
+
if results.any?
|
|
52
|
+
LOG.info "[Notify] #{event} delivered via #{channel} to #{target}"
|
|
53
|
+
true
|
|
54
|
+
else
|
|
55
|
+
LOG.warn "[Notify] No handler responded for channel '#{channel}' (is the plugin installed?)"
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
LOG.error "[Notify] Failed to send '#{event}': #{e.message}"
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the notification config for a specific event type.
|
|
64
|
+
# Falls back to "default" config if no event-specific config exists.
|
|
65
|
+
def notification_config_for(event)
|
|
66
|
+
brainiac_config_file = File.join(BRAINIAC_DIR, "brainiac.json")
|
|
67
|
+
return {} unless File.exist?(brainiac_config_file)
|
|
68
|
+
|
|
69
|
+
config = JSON.parse(File.read(brainiac_config_file))
|
|
70
|
+
notifications = config[NOTIFICATIONS_CONFIG_KEY] || {}
|
|
71
|
+
|
|
72
|
+
notifications[event.to_s] || notifications["default"] || {}
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Convenience: send a notification for cron job output.
|
|
78
|
+
def notify_cron_output(job, message, agent_name: nil)
|
|
79
|
+
send_notification(:cron, message,
|
|
80
|
+
target: job[:notify_target] || job[:discord_channel_id],
|
|
81
|
+
channel: job[:notify_channel] || (job[:discord_channel_id] ? :discord : nil),
|
|
82
|
+
agent: agent_name || job[:agent],
|
|
83
|
+
job_id: job[:id],
|
|
84
|
+
forum_title: job[:forum_title],
|
|
85
|
+
forum_reply_to_latest: job[:forum_reply_to_latest])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Convenience: send a deploy notification.
|
|
89
|
+
def notify_deploy(project_key, message)
|
|
90
|
+
send_notification(:deploy, message, metadata_project: project_key)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convenience: send a restart notification.
|
|
94
|
+
def notify_restart(message)
|
|
95
|
+
send_notification(:restart, message)
|
|
96
|
+
end
|
data/lib/brainiac/prompts.rb
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Prompts are layered:
|
|
6
6
|
# PROMPT_CORE — universal (identity, memory, brain, reflection)
|
|
7
|
-
# PROMPT_DISCORD_CHANNEL — Discord-specific rules (markdown, response file, char limits)
|
|
8
7
|
# PROMPT_GITHUB_CHANNEL — GitHub-specific rules (GFM, PR conventions)
|
|
9
8
|
#
|
|
10
9
|
# Each handler composes: PROMPT_CORE + channel rules + situation template.
|
|
10
|
+
# Channel-specific prompts (Discord, Fizzy, etc.) are registered by plugins.
|
|
11
11
|
|
|
12
12
|
# ---------------------------------------------------------------------------
|
|
13
13
|
# PROMPT_CORE — included in EVERY session regardless of channel
|
|
@@ -186,87 +186,6 @@ PROMPT_REFLECTION = <<~PROMPT
|
|
|
186
186
|
|
|
187
187
|
PROMPT
|
|
188
188
|
|
|
189
|
-
# ---------------------------------------------------------------------------
|
|
190
|
-
# PROMPT_DISCORD_CHANNEL — Discord-specific rules, prepended to Discord templates
|
|
191
|
-
# ---------------------------------------------------------------------------
|
|
192
|
-
PROMPT_DISCORD_CHANNEL = <<~PROMPT
|
|
193
|
-
## Discord Channel Rules
|
|
194
|
-
|
|
195
|
-
### Mentions
|
|
196
|
-
Discord does NOT support plain-text @mentions. Writing `@Galen` renders as plain text.
|
|
197
|
-
To actually mention someone, use the `<@USER_ID>` format. Here are the known IDs:
|
|
198
|
-
{{DISCORD_MENTION_ROSTER}}
|
|
199
|
-
|
|
200
|
-
If you need to mention someone not on this list, just write their name without the @ symbol.
|
|
201
|
-
Do NOT @mention other agent bots unless the user explicitly asks you to bring them into the conversation.
|
|
202
|
-
Mentioning another agent triggers an automated dispatch — doing it casually can cause loops.
|
|
203
|
-
|
|
204
|
-
### Formatting
|
|
205
|
-
Do NOT use HTML formatting. Use plain text or Discord markdown:
|
|
206
|
-
- ```code blocks``` for code
|
|
207
|
-
- **bold** for emphasis
|
|
208
|
-
- *italic* for softer emphasis
|
|
209
|
-
- > quotes for referencing
|
|
210
|
-
|
|
211
|
-
### Response Delivery
|
|
212
|
-
You MUST write your response to a file at `{{RESPONSE_FILE}}`.
|
|
213
|
-
Do NOT respond via stdout — your response will only be delivered if written to this file.
|
|
214
|
-
Keep it conversational and concise — Discord messages have a 2000 char limit
|
|
215
|
-
per message, though long responses will be split automatically.
|
|
216
|
-
|
|
217
|
-
### Scope
|
|
218
|
-
This is a conversational interaction — no card, no PR. You're here to answer questions,
|
|
219
|
-
discuss code, share knowledge, or help with whatever the user needs.
|
|
220
|
-
|
|
221
|
-
**Detect user intent:**
|
|
222
|
-
- If they're asking you to **implement, fix, build, update, or change** something → do the work
|
|
223
|
-
- If they're asking questions, discussing ideas, or seeking advice → respond conversationally
|
|
224
|
-
|
|
225
|
-
**When doing implementation work:**
|
|
226
|
-
1. Create a worktree branching from `origin/main` (or the default branch shown in Project Context):
|
|
227
|
-
`git worktree add -b discord-<topic>-<timestamp> ../<repo>--discord-<topic>-<timestamp> origin/main`
|
|
228
|
-
2. `cd` into the new worktree directory
|
|
229
|
-
3. Make the changes, test if applicable
|
|
230
|
-
4. Commit with a clear message
|
|
231
|
-
5. Push the branch
|
|
232
|
-
6. Summarize what you did in your response file
|
|
233
|
-
7. If it's substantial or needs review, mention opening a PR (but don't create it unless asked)
|
|
234
|
-
|
|
235
|
-
**When responding conversationally:**
|
|
236
|
-
- Answer questions about the codebase, architecture, conventions
|
|
237
|
-
- Search your brain (knowledge + persona) for relevant context
|
|
238
|
-
- Read files from registered project repos to investigate questions
|
|
239
|
-
- Update your knowledge or persona files if the conversation warrants it
|
|
240
|
-
|
|
241
|
-
### GIFs (optional)
|
|
242
|
-
You can optionally include a GIF in your Discord response to add personality.
|
|
243
|
-
To find one, search the local GIF API:
|
|
244
|
-
```
|
|
245
|
-
curl -s "http://localhost:4567/api/gif?q=your+search+terms"
|
|
246
|
-
```
|
|
247
|
-
This returns JSON with a `results` array. Each result has a `url` field — paste that
|
|
248
|
-
URL on its own line in your response and Discord will auto-embed it as an animated GIF.
|
|
249
|
-
|
|
250
|
-
**Guidelines:**
|
|
251
|
-
- GIFs should be RARE — include one in roughly 15% of responses, not more
|
|
252
|
-
- 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
|
|
253
|
-
- Skip GIFs for routine answers, technical implementation work, status updates, or when the tone doesn't call for one
|
|
254
|
-
- Match the GIF to the emotional tone — celebration, sarcasm, emphasis, humor
|
|
255
|
-
- Surprise is good — pick GIFs that are unexpected or perfectly timed, not generic
|
|
256
|
-
- Pick the most relevant result, not just the first one
|
|
257
|
-
- If the API returns no results or errors, just skip the GIF — don't mention it
|
|
258
|
-
|
|
259
|
-
### Thread Memory (CRITICAL for long conversations)
|
|
260
|
-
Discord threads drift — your context window only shows recent messages, not the full history.
|
|
261
|
-
When writing your memory file for a Discord thread session, you MUST include:
|
|
262
|
-
- The original question/topic that started the thread (from "Original Message" above or your prior memory)
|
|
263
|
-
- A condensed summary of ALL topics discussed so far, not just this session
|
|
264
|
-
- Any topic shifts that occurred — what changed and why
|
|
265
|
-
- The current topic/focus as of this session
|
|
266
|
-
This is the ONLY way future sessions will know what happened in the middle of the conversation.
|
|
267
|
-
|
|
268
|
-
PROMPT
|
|
269
|
-
|
|
270
189
|
# ---------------------------------------------------------------------------
|
|
271
190
|
# PROMPT_GITHUB_CHANNEL — GitHub-specific rules, prepended to GitHub templates
|
|
272
191
|
# ---------------------------------------------------------------------------
|
|
@@ -286,24 +205,6 @@ PROMPT_GITHUB_CHANNEL = <<~PROMPT
|
|
|
286
205
|
|
|
287
206
|
PROMPT
|
|
288
207
|
|
|
289
|
-
PROMPT_DISCORD = <<~'PROMPT'
|
|
290
|
-
## Context
|
|
291
|
-
|
|
292
|
-
**From:** {{DISCORD_USER}} in #{{CHANNEL_NAME}}
|
|
293
|
-
{{REPLY_CONTEXT}}**Message:**
|
|
294
|
-
{{MESSAGE_BODY}}
|
|
295
|
-
|
|
296
|
-
{{THREAD_ROOT_CONTEXT}}### Recent Channel History
|
|
297
|
-
These are the messages immediately before the one above, for conversational context:
|
|
298
|
-
```
|
|
299
|
-
{{CHANNEL_HISTORY}}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
{{PROJECT_CONTEXT}}
|
|
303
|
-
|
|
304
|
-
**IMPORTANT: Write your response to `{{RESPONSE_FILE}}`. Do NOT reply via stdout.**
|
|
305
|
-
PROMPT
|
|
306
|
-
|
|
307
208
|
PROMPT_GITHUB_PR_COMMENT = <<~'PROMPT'
|
|
308
209
|
There's a new comment from @{{COMMENT_CREATOR}} on your PR #{{PR_NUMBER}} for card #{{CARD_NUMBER}}.
|
|
309
210
|
|
|
@@ -355,7 +256,6 @@ PROMPT_GITHUB_UAT = <<~'PROMPT'
|
|
|
355
256
|
PROMPT
|
|
356
257
|
|
|
357
258
|
CHANNEL_PROMPTS = {
|
|
358
|
-
discord: PROMPT_DISCORD_CHANNEL,
|
|
359
259
|
github: PROMPT_GITHUB_CHANNEL
|
|
360
260
|
}.freeze
|
|
361
261
|
|
|
@@ -374,7 +274,7 @@ def render_prompt(template, vars = {}, brain_context: "", card_context: "", agen
|
|
|
374
274
|
|
|
375
275
|
# Channel prompt: check plugin-registered prompts first, then built-in
|
|
376
276
|
plugin_prompt = Brainiac.channel_prompts[channel]
|
|
377
|
-
result += plugin_prompt || CHANNEL_PROMPTS.fetch(channel,
|
|
277
|
+
result += plugin_prompt || CHANNEL_PROMPTS.fetch(channel, "")
|
|
378
278
|
|
|
379
279
|
result += template
|
|
380
280
|
|
data/lib/brainiac/restart.rb
CHANGED
|
@@ -22,17 +22,9 @@ def queue_brainiac_restart(agent_name)
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# Send a
|
|
25
|
+
# Send a notification about brainiac restart/startup.
|
|
26
26
|
def send_restart_notification(message)
|
|
27
|
-
|
|
28
|
-
return unless channel_id
|
|
29
|
-
|
|
30
|
-
tokens = discord_bot_tokens
|
|
31
|
-
triggered_by = BRAINIAC_RESTART_MUTEX.synchronize { BRAINIAC_RESTART_STATE[:triggered_by] }
|
|
32
|
-
token = tokens[triggered_by&.downcase] || tokens.values.first
|
|
33
|
-
return unless token
|
|
34
|
-
|
|
35
|
-
send_discord_message(channel_id, message, token: token)
|
|
27
|
+
notify_restart(message)
|
|
36
28
|
rescue StandardError => e
|
|
37
29
|
LOG.warn "[Brainiac] Failed to send restart notification: #{e.message}"
|
|
38
30
|
end
|
data/lib/brainiac/routes/api.rb
CHANGED
|
@@ -283,23 +283,11 @@ get "/api/logs" do
|
|
|
283
283
|
all_lines.join.gsub(/\e\[[\d;]*[a-zA-Z]/, "").gsub(/\e\[\?[\d;]*[a-zA-Z]/, "")
|
|
284
284
|
end
|
|
285
285
|
|
|
286
|
-
# --- GIF ---
|
|
286
|
+
# --- GIF (handled by discord plugin when installed) ---
|
|
287
287
|
|
|
288
288
|
get "/api/gif" do
|
|
289
289
|
content_type :json
|
|
290
|
-
|
|
291
|
-
halt 400, { error: "Missing ?q= parameter" }.to_json if query.empty?
|
|
292
|
-
|
|
293
|
-
api_key = DISCORD_CONFIG["giphy_api_key"]
|
|
294
|
-
halt 503, { error: "No giphy_api_key configured in discord.json" }.to_json unless api_key
|
|
295
|
-
|
|
296
|
-
gifs = search_giphy(query, api_key)
|
|
297
|
-
halt 502, { error: "Giphy API error" }.to_json unless gifs
|
|
298
|
-
|
|
299
|
-
{ query: query, results: gifs }.to_json
|
|
300
|
-
rescue StandardError => e
|
|
301
|
-
LOG.error "[GIF] Search failed: #{e.message}"
|
|
302
|
-
halt 500, { error: e.message }.to_json
|
|
290
|
+
halt 503, { error: "brainiac-discord plugin not installed (provides GIF API)" }.to_json
|
|
303
291
|
end
|
|
304
292
|
|
|
305
293
|
# --- Cron ---
|
data/lib/brainiac/version.rb
CHANGED
data/receiver.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "lib/brainiac/skills"
|
|
|
21
21
|
require_relative "lib/brainiac/sessions"
|
|
22
22
|
require_relative "lib/brainiac/prompts"
|
|
23
23
|
require_relative "lib/brainiac/helpers"
|
|
24
|
+
require_relative "lib/brainiac/notifications"
|
|
24
25
|
require_relative "lib/brainiac/cron"
|
|
25
26
|
require_relative "lib/brainiac/restart"
|
|
26
27
|
require_relative "lib/brainiac/plugins"
|
|
@@ -40,12 +41,7 @@ if handler_enabled?("github")
|
|
|
40
41
|
LOG.info "[Handlers] GitHub handler enabled"
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
if
|
|
44
|
-
require_relative "lib/brainiac/handlers/discord"
|
|
45
|
-
LOG.info "[Handlers] Discord handler enabled"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
if DISCORD_ENABLED && handler_enabled?("zoho")
|
|
44
|
+
if handler_enabled?("zoho")
|
|
49
45
|
require_relative "lib/brainiac/handlers/zoho"
|
|
50
46
|
LOG.info "[Handlers] Zoho handler enabled"
|
|
51
47
|
end
|
|
@@ -218,7 +214,7 @@ end
|
|
|
218
214
|
|
|
219
215
|
# --- Zoho Mail webhook route ---
|
|
220
216
|
|
|
221
|
-
if
|
|
217
|
+
if handler_enabled?("zoho")
|
|
222
218
|
post "/zoho" do
|
|
223
219
|
content_type :json
|
|
224
220
|
request.body.rewind
|
|
@@ -271,7 +267,7 @@ end
|
|
|
271
267
|
|
|
272
268
|
# --- Zoho OAuth routes ---
|
|
273
269
|
|
|
274
|
-
if
|
|
270
|
+
if handler_enabled?("zoho")
|
|
275
271
|
ZOHO_AUTH_SCOPES = "ZohoMail.messages.READ,ZohoMail.accounts.READ,ZohoMail.folders.READ".freeze
|
|
276
272
|
|
|
277
273
|
get "/zoho/auth" do
|
|
@@ -368,64 +364,17 @@ get "/dashboard" do
|
|
|
368
364
|
erb :dashboard, layout: false
|
|
369
365
|
end
|
|
370
366
|
|
|
371
|
-
# --- Discord
|
|
372
|
-
|
|
373
|
-
if DISCORD_ENABLED
|
|
374
|
-
get "/api/discord" do
|
|
375
|
-
content_type :json
|
|
376
|
-
{
|
|
377
|
-
enabled: true,
|
|
378
|
-
bots: discord_bots_status,
|
|
379
|
-
config: {
|
|
380
|
-
default_project: DISCORD_CONFIG["default_project"],
|
|
381
|
-
channel_mappings: DISCORD_CONFIG["channel_mappings"]&.size || 0,
|
|
382
|
-
authorized_users: (DISCORD_CONFIG["authorized_user_ids"] || []).size,
|
|
383
|
-
authorized_roles: (DISCORD_CONFIG["authorized_role_ids"] || []).size
|
|
384
|
-
}
|
|
385
|
-
}.to_json
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
start_all_discord_gateways
|
|
389
|
-
start_discord_draft_poller
|
|
390
|
-
start_brainiac_restart_monitor
|
|
391
|
-
|
|
392
|
-
# Send "back online" notification after bots connect (if restarted)
|
|
393
|
-
Thread.new do
|
|
394
|
-
# Wait for at least one bot to be ready (up to 30s)
|
|
395
|
-
30.times do
|
|
396
|
-
sleep 1
|
|
397
|
-
ready = DISCORD_BOTS_MUTEX.synchronize { DISCORD_BOTS.any? { |_, info| info[:status] == "ready" } }
|
|
398
|
-
next unless ready
|
|
399
|
-
|
|
400
|
-
send_restart_notification("✅ Brainiac back online")
|
|
401
|
-
|
|
402
|
-
# Check if running an outdated version (skip in dev/foreground mode)
|
|
403
|
-
unless $stdout.tty?
|
|
404
|
-
version_info = check_brainiac_version
|
|
405
|
-
if version_info[:behind]
|
|
406
|
-
owner_id = owner_discord_id
|
|
407
|
-
mention = owner_id ? "<@#{owner_id}>" : "Someone"
|
|
408
|
-
channel_id = DISCORD_CONFIG["notification_channel_id"]
|
|
409
|
-
tokens = discord_bot_tokens
|
|
410
|
-
token = tokens.values.first
|
|
411
|
-
if channel_id && token
|
|
412
|
-
send_discord_message(channel_id,
|
|
413
|
-
"#{mention}: Brainiac was updated and needs to be pulled down (#{version_info[:commits_behind]} commit#{"s" if version_info[:commits_behind] != 1} behind, running #{version_info[:local_sha]} vs #{version_info[:remote_sha]})",
|
|
414
|
-
token: token)
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
end
|
|
367
|
+
# --- Discord fallback (plugin handles startup when installed) ---
|
|
418
368
|
|
|
419
|
-
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
else
|
|
369
|
+
unless Brainiac.channel_prompts[:discord]
|
|
423
370
|
get "/api/discord" do
|
|
424
371
|
content_type :json
|
|
425
|
-
{ enabled: false, reason: "
|
|
372
|
+
{ enabled: false, reason: "brainiac-discord plugin not installed" }.to_json
|
|
426
373
|
end
|
|
427
374
|
end
|
|
428
375
|
|
|
376
|
+
start_brainiac_restart_monitor
|
|
377
|
+
|
|
429
378
|
LOG.info "[Cron] Starting cron thread..."
|
|
430
379
|
start_cron_thread
|
|
431
380
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainiac-plugins
|
|
3
|
+
description: |
|
|
4
|
+
Develop Brainiac plugins — gem-based handlers that extend Brainiac with new
|
|
5
|
+
communication channels, integrations, or features. Covers the plugin contract,
|
|
6
|
+
file structure, hooks, CLI commands, and the generator.
|
|
7
|
+
triggers:
|
|
8
|
+
- brainiac plugin
|
|
9
|
+
- create plugin
|
|
10
|
+
- new plugin
|
|
11
|
+
- plugin development
|
|
12
|
+
- plugin contract
|
|
13
|
+
- plugin hooks
|
|
14
|
+
- brainiac handler
|
|
15
|
+
- extend brainiac
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Brainiac Plugin Development
|
|
19
|
+
|
|
20
|
+
Plugins are Ruby gems named `brainiac-<name>` that extend Brainiac without modifying core.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
brainiac plugin new my-integration
|
|
26
|
+
cd brainiac-my-integration
|
|
27
|
+
bundle install
|
|
28
|
+
rake test # Verify scaffold works
|
|
29
|
+
# Implement your logic...
|
|
30
|
+
brainiac install my-integration --path .
|
|
31
|
+
brainiac restart
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Plugin Contract
|
|
35
|
+
|
|
36
|
+
A valid Brainiac plugin MUST:
|
|
37
|
+
|
|
38
|
+
1. **Gem named `brainiac-<name>`** — e.g. `brainiac-discord`, `brainiac-slack`
|
|
39
|
+
2. **Entry file at `lib/brainiac_<name>.rb`** — loaded by RubyGems, requires the module
|
|
40
|
+
3. **Module at `Brainiac::Plugins::<Name>`** — PascalCase (e.g. `Discord`, `TestWidget`)
|
|
41
|
+
4. **Implement `.register(app)`** — receives `Sinatra::Application` at server startup
|
|
42
|
+
|
|
43
|
+
A plugin SHOULD also provide:
|
|
44
|
+
|
|
45
|
+
| Method | File | Purpose |
|
|
46
|
+
|--------|------|---------|
|
|
47
|
+
| `.register(app)` | main module | **Required.** Server startup — define routes, hooks, threads |
|
|
48
|
+
| `.configured?` | `metadata.rb` | Returns false → auto-runs setup on `brainiac install` |
|
|
49
|
+
| `.help_text` | `metadata.rb` | One-liner shown in `brainiac help` |
|
|
50
|
+
| `.cli(args)` | `cli.rb` | CLI subcommands via `brainiac <plugin> ...` |
|
|
51
|
+
|
|
52
|
+
## File Structure
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
brainiac-<name>/
|
|
56
|
+
├── lib/
|
|
57
|
+
│ ├── brainiac_<name>.rb # Entry point (require the module)
|
|
58
|
+
│ └── brainiac/plugins/<name>/
|
|
59
|
+
│ ├── version.rb # VERSION constant
|
|
60
|
+
│ ├── metadata.rb # help_text, configured? (lightweight, no deps)
|
|
61
|
+
│ ├── cli.rb # CLI subcommands (standalone, no server deps)
|
|
62
|
+
│ └── (your modules).rb # Server runtime code
|
|
63
|
+
├── test/
|
|
64
|
+
│ ├── test_helper.rb
|
|
65
|
+
│ └── test_<name>.rb
|
|
66
|
+
├── brainiac-<name>.gemspec
|
|
67
|
+
├── Gemfile, Rakefile, .rubocop.yml, README.md
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Critical Architecture: CLI vs Server Runtime
|
|
71
|
+
|
|
72
|
+
Plugins are loaded in TWO different contexts:
|
|
73
|
+
|
|
74
|
+
1. **Server context** — full plugin loaded via `.register(app)`. Has access to `LOG`, `AGENT_REGISTRY`, `PROJECTS`, `ACTIVE_SESSIONS`, and all core functions.
|
|
75
|
+
|
|
76
|
+
2. **CLI context** — only `metadata.rb` and `cli.rb` are loaded. NO server runtime available. These files MUST NOT require any module that depends on server constants.
|
|
77
|
+
|
|
78
|
+
This means:
|
|
79
|
+
- `metadata.rb` requires only `version.rb`
|
|
80
|
+
- `cli.rb` uses only stdlib (`json`, `net/http`, `fileutils`)
|
|
81
|
+
- Everything else (config, handlers, hooks) lives in separate files loaded only by `.register(app)`
|
|
82
|
+
|
|
83
|
+
## Hooks
|
|
84
|
+
|
|
85
|
+
Core emits lifecycle events. Subscribe in `.register(app)`:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
def register(app)
|
|
89
|
+
Brainiac.on(:agent_completed) do |ctx|
|
|
90
|
+
# ctx has :agent_name, :card_number, :exit_status, etc.
|
|
91
|
+
move_card(ctx[:card_number], "done")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Available hooks:
|
|
97
|
+
|
|
98
|
+
| Hook | When | Context |
|
|
99
|
+
|------|------|---------|
|
|
100
|
+
| `:server_started` | All plugins loaded | — |
|
|
101
|
+
| `:pre_dispatch` | Before agent CLI spawned | agent_name, project_config |
|
|
102
|
+
| `:agent_completed` | Agent session finished | agent_name, exit_status, source, card_number |
|
|
103
|
+
| `:agent_crashed` | Agent process crashed | agent_name, exit_status, log_file |
|
|
104
|
+
| `:build_brain_context` | Building prompt context | agent_name, card_title |
|
|
105
|
+
| `:pr_merged` | GitHub PR merged | pr_number, repo, branch |
|
|
106
|
+
| `:pr_review_received` | PR review submitted | pr_number, reviewer |
|
|
107
|
+
| `:pr_synchronized` | PR updated (new commits) | pr_number |
|
|
108
|
+
| `:production_deployed` | Deploy workflow succeeded | repo, environment |
|
|
109
|
+
| `:create_work_item` | Create a card/issue/ticket | title, body, project |
|
|
110
|
+
| `:detect_cli_provider` | Detect CLI provider | metadata hash |
|
|
111
|
+
| `:detect_effort` | Detect effort level | metadata hash |
|
|
112
|
+
|
|
113
|
+
## Channel Prompts
|
|
114
|
+
|
|
115
|
+
If your plugin is a communication channel (like Discord, Fizzy), register a prompt:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
def register(app)
|
|
119
|
+
Brainiac.register_channel_prompt(:my_channel, MY_CHANNEL_PROMPT,
|
|
120
|
+
pre_post_check: MY_PRE_POST_CHECK)
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The prompt is prepended to every agent session dispatched through your channel. Use `{{PLACEHOLDERS}}` that `render_prompt` will fill.
|
|
125
|
+
|
|
126
|
+
## Routes
|
|
127
|
+
|
|
128
|
+
Define webhook and API routes on the Sinatra app:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
def register(app)
|
|
132
|
+
app.post "/my-webhook" do
|
|
133
|
+
content_type :json
|
|
134
|
+
payload = JSON.parse(request.body.read)
|
|
135
|
+
# Handle webhook...
|
|
136
|
+
{ status: "ok" }.to_json
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
app.get "/api/my-plugin" do
|
|
140
|
+
content_type :json
|
|
141
|
+
{ enabled: true }.to_json
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## CLI Module Pattern
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
module Brainiac::Plugins::MyPlugin
|
|
150
|
+
module Cli
|
|
151
|
+
class << self
|
|
152
|
+
def run(args)
|
|
153
|
+
case args.shift
|
|
154
|
+
when "setup" then cmd_setup
|
|
155
|
+
when "config" then cmd_config
|
|
156
|
+
else print_help
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.cli(args)
|
|
163
|
+
Cli.run(args)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
CLI commands should manage config files at `~/.brainiac/<name>.json` and query the server API for status. They must NOT load server runtime modules.
|
|
169
|
+
|
|
170
|
+
## Core Functions Available in Server Context
|
|
171
|
+
|
|
172
|
+
When your plugin's `.register(app)` runs, these are available:
|
|
173
|
+
|
|
174
|
+
| Function | Purpose |
|
|
175
|
+
|----------|---------|
|
|
176
|
+
| `agent_display_name(key)` | Get display name for agent |
|
|
177
|
+
| `agent_env_for(name)` | Get env vars hash for agent |
|
|
178
|
+
| `AGENT_REGISTRY` | Hash of all agents |
|
|
179
|
+
| `PROJECTS` | Hash of all projects |
|
|
180
|
+
| `LOG` | Logger instance |
|
|
181
|
+
| `BRAINIAC_DIR` | Path to `~/.brainiac/` |
|
|
182
|
+
| `register_session(key, pid, **)` | Track active session |
|
|
183
|
+
| `session_active?(key)` | Check if session running |
|
|
184
|
+
| `build_brain_context(...)` | Build brain context for prompt |
|
|
185
|
+
| `render_prompt(template, vars, ...)` | Compose full prompt |
|
|
186
|
+
| `run_agent(...)` | Spawn agent CLI process |
|
|
187
|
+
| `detect_model(config, text:)` | Detect model from inline tags |
|
|
188
|
+
| `detect_effort(config, text:)` | Detect effort from inline tags |
|
|
189
|
+
| `parse_inline_tags(text)` | Parse [model], [project:X], etc. |
|
|
190
|
+
| `reload_projects!` | Reload projects.json |
|
|
191
|
+
| `reload_agent_registry!` | Reload agents.json |
|
|
192
|
+
| `brain_push(message:)` | Push brain changes to git |
|
|
193
|
+
|
|
194
|
+
## Testing
|
|
195
|
+
|
|
196
|
+
Generated test helpers stub all core constants and functions. Tests run without a server:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
rake test # Run minitest
|
|
200
|
+
rake rubocop # Run linter
|
|
201
|
+
rake # Both
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Publishing
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
gem build brainiac-<name>.gemspec
|
|
208
|
+
gem push brainiac-<name>-0.0.1.gem
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Users install with:
|
|
212
|
+
```bash
|
|
213
|
+
brainiac install <name>
|
|
214
|
+
brainiac restart
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Reference Implementations
|
|
218
|
+
|
|
219
|
+
- `brainiac-discord` — Full communication channel (gateway, message handler, delivery, reactions, CLI)
|
|
220
|
+
- `brainiac-fizzy` — Card management (webhooks, hooks, duplicate detection, planning mode, CLI)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainiac
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -51,20 +51,6 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '4.1'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: websocket-client-simple
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.8.0
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.8.0
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
55
|
name: minitest
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -121,8 +107,8 @@ dependencies:
|
|
|
121
107
|
- - "~>"
|
|
122
108
|
- !ruby/object:Gem::Version
|
|
123
109
|
version: '1.25'
|
|
124
|
-
description: Webhook receiver that listens for
|
|
125
|
-
|
|
110
|
+
description: Webhook receiver that listens for GitHub and Zoho Mail events, then dispatches
|
|
111
|
+
work to AI agent CLIs. Additional channels (Discord, Fizzy) available via plugins.
|
|
126
112
|
executables:
|
|
127
113
|
- brainiac
|
|
128
114
|
extensions: []
|
|
@@ -142,19 +128,13 @@ files:
|
|
|
142
128
|
- lib/brainiac/brain.rb
|
|
143
129
|
- lib/brainiac/config.rb
|
|
144
130
|
- lib/brainiac/cron.rb
|
|
145
|
-
- lib/brainiac/handlers/discord.rb
|
|
146
|
-
- lib/brainiac/handlers/discord/api.rb
|
|
147
|
-
- lib/brainiac/handlers/discord/config.rb
|
|
148
|
-
- lib/brainiac/handlers/discord/delivery.rb
|
|
149
|
-
- lib/brainiac/handlers/discord/gateway.rb
|
|
150
|
-
- lib/brainiac/handlers/discord/message.rb
|
|
151
|
-
- lib/brainiac/handlers/discord/reactions.rb
|
|
152
131
|
- lib/brainiac/handlers/github.rb
|
|
153
132
|
- lib/brainiac/handlers/shared/git.rb
|
|
154
133
|
- lib/brainiac/handlers/shared/inline_tags.rb
|
|
155
134
|
- lib/brainiac/handlers/zoho.rb
|
|
156
135
|
- lib/brainiac/helpers.rb
|
|
157
136
|
- lib/brainiac/hooks.rb
|
|
137
|
+
- lib/brainiac/notifications.rb
|
|
158
138
|
- lib/brainiac/plugins.rb
|
|
159
139
|
- lib/brainiac/prompts.rb
|
|
160
140
|
- lib/brainiac/restart.rb
|
|
@@ -176,11 +156,11 @@ files:
|
|
|
176
156
|
- monitor/xbar/setup.rb
|
|
177
157
|
- monitor/xbar/view_logs.rb
|
|
178
158
|
- receiver.rb
|
|
159
|
+
- skills/brainiac-plugins/SKILL.md
|
|
179
160
|
- templates/agents.json.example
|
|
180
161
|
- templates/brainiac.json.example
|
|
181
162
|
- templates/cli-providers/grok.json.example
|
|
182
163
|
- templates/cli-providers/kiro.json.example
|
|
183
|
-
- templates/discord.json.example
|
|
184
164
|
- templates/github.json.example
|
|
185
165
|
- templates/plugins.json.example
|
|
186
166
|
- templates/roles/code-reviewer.md.example
|