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.
@@ -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
@@ -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, PROMPT_DISCORD_CHANNEL)
277
+ result += plugin_prompt || CHANNEL_PROMPTS.fetch(channel, "")
378
278
 
379
279
  result += template
380
280
 
@@ -22,17 +22,9 @@ def queue_brainiac_restart(agent_name)
22
22
  end
23
23
  end
24
24
 
25
- # Send a Discord notification about brainiac restart/startup using any available bot token.
25
+ # Send a notification about brainiac restart/startup.
26
26
  def send_restart_notification(message)
27
- channel_id = DISCORD_CONFIG["notification_channel_id"]
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
@@ -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
- query = params[:q].to_s.strip
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 ---
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.9"
5
+ VERSION = "0.0.11"
6
6
  end
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 DISCORD_ENABLED && handler_enabled?("discord")
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 DISCORD_ENABLED
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 DISCORD_ENABLED
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 API + startup ---
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
- break
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: "websocket-client-simple gem not installed" }.to_json
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.9
4
+ version: 0.0.11
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 Fizzy, GitHub, Discord, and Zoho Mail
125
- events, then dispatches work to AI agent CLIs.
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