brainiac 0.0.10 → 0.0.12

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.
data/receiver.rb CHANGED
@@ -34,17 +34,7 @@ module Brainiac
34
34
  end
35
35
  end
36
36
 
37
- # --- Conditional handler loading (based on ~/.brainiac/brainiac.json) ---
38
-
39
- if handler_enabled?("github")
40
- require_relative "lib/brainiac/handlers/github"
41
- LOG.info "[Handlers] GitHub handler enabled"
42
- end
43
-
44
- if handler_enabled?("zoho")
45
- require_relative "lib/brainiac/handlers/zoho"
46
- LOG.info "[Handlers] Zoho handler enabled"
47
- end
37
+ # --- Custom handlers + plugins ---
48
38
 
49
39
  # Reload hook registry — custom handlers register callbacks here
50
40
  module ReloadHooks
@@ -63,15 +53,11 @@ def register_reload_hook(name, &)
63
53
  ReloadHooks.register(name, &)
64
54
  end
65
55
 
66
- # Load custom handlers from ~/.brainiac/handlers/ (plugin system)
56
+ # Load custom handlers from ~/.brainiac/handlers/ (legacy plugin system)
67
57
  CUSTOM_HANDLERS_DIR = File.join(BRAINIAC_DIR, "handlers")
68
58
  if Dir.exist?(CUSTOM_HANDLERS_DIR)
69
59
  Dir.glob(File.join(CUSTOM_HANDLERS_DIR, "*.rb")).each do |handler|
70
60
  handler_name = File.basename(handler, ".rb")
71
- unless handler_enabled?(handler_name)
72
- LOG.info "[Handlers] Skipping custom handler (disabled): #{handler_name}"
73
- next
74
- end
75
61
  LOG.info "[Handlers] Loading custom handler: #{handler_name}"
76
62
  require handler
77
63
  end
@@ -136,211 +122,9 @@ end
136
122
  before "/api/*" do
137
123
  # Skip auth for all localhost requests (CLI, waybar, daemon, etc.)
138
124
  pass if localhost_request?
139
- # Skip auth for webhook-related routes that have their own verification
140
- pass if request.path_info == "/api/discord"
141
125
  authenticate_dashboard!
142
126
  end
143
127
 
144
- post "/github" do
145
- content_type :json
146
- request.body.rewind
147
- payload_body = request.body.read
148
-
149
- verify_github_signature!(request, payload_body)
150
-
151
- payload = JSON.parse(payload_body)
152
- event = request.env["HTTP_X_GITHUB_EVENT"]
153
-
154
- reload_projects!
155
- reload_agent_registry!
156
- reload_github_config!
157
-
158
- action = payload["action"]
159
-
160
- case event
161
- when "pull_request"
162
- if action == "closed" && payload.dig("pull_request", "merged")
163
- status_code, body = handle_github_pr_merged(payload)
164
- halt status_code, body
165
- elsif action == "opened"
166
- track_pr_in_work_items(payload)
167
- halt 200, { status: "processed", action: "pr_tracked" }.to_json
168
- elsif action == "synchronize"
169
- status_code, body = handle_github_pr_synchronized(payload)
170
- halt status_code, body
171
- else
172
- halt 200, { status: "ignored", reason: "pull_request action: #{action}" }.to_json
173
- end
174
- when "pull_request_review"
175
- if action == "submitted"
176
- status_code, body = handle_github_pr_review_submitted(payload)
177
- halt status_code, body
178
- else
179
- halt 200, { status: "ignored", reason: "pull_request_review action: #{action}" }.to_json
180
- end
181
- when "issue_comment"
182
- if action == "created"
183
- status_code, body = handle_github_issue_comment(payload)
184
- halt status_code, body
185
- else
186
- halt 200, { status: "ignored", reason: "issue_comment action: #{action}" }.to_json
187
- end
188
- when "issues"
189
- if action == "opened"
190
- status_code, body = handle_github_issue_opened(payload)
191
- halt status_code, body
192
- else
193
- halt 200, { status: "ignored", reason: "issues action: #{action}" }.to_json
194
- end
195
- when "workflow_run"
196
- if action == "completed"
197
- status_code, body = handle_github_workflow_run(payload)
198
- halt status_code, body
199
- else
200
- halt 200, { status: "ignored", reason: "workflow_run action: #{action}" }.to_json
201
- end
202
- when "ping"
203
- halt 200, { status: "pong" }.to_json
204
- else
205
- halt 200, { status: "ignored", event: event }.to_json
206
- end
207
- rescue JSON::ParserError => e
208
- LOG.error "Invalid JSON: #{e.message}"
209
- halt 400, { error: "Invalid JSON" }.to_json
210
- rescue StandardError => e
211
- LOG.error "Unhandled error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
212
- halt 500, { error: e.message }.to_json
213
- end
214
-
215
- # --- Zoho Mail webhook route ---
216
-
217
- if handler_enabled?("zoho")
218
- post "/zoho" do
219
- content_type :json
220
- request.body.rewind
221
- payload_body = request.body.read
222
-
223
- # Zoho sends X-Hook-Secret on the very first request — capture and store it
224
- hook_secret = request.env["HTTP_X_HOOK_SECRET"]
225
- if hook_secret
226
- save_zoho_hook_secret(hook_secret)
227
- LOG.info "[Zoho] Received and stored hook_secret from initial handshake"
228
- halt 200, { status: "hook_secret_received" }.to_json
229
- end
230
-
231
- verify_zoho_signature!(request, payload_body)
232
-
233
- email = JSON.parse(payload_body)
234
- LOG.info "[Zoho] Received email: subject=#{email["subject"]}, from=#{email["fromAddress"]}, to=#{email["toAddress"]}"
235
- LOG.info "[Zoho] Payload keys: #{email.keys.sort.join(", ")}"
236
- LOG.info "[Zoho] summary=#{email["summary"].to_s[0..200].inspect}, html=#{email["html"].to_s[0..200].inspect}, content=#{email["content"].to_s[0..200].inspect}"
237
-
238
- # Dump raw payload for debugging (last 5 kept)
239
- zoho_debug_dir = File.join(BRAINIAC_DIR, "tmp", "zoho", "payloads")
240
- FileUtils.mkdir_p(zoho_debug_dir)
241
- File.write(File.join(zoho_debug_dir, "#{Time.now.strftime("%Y%m%d-%H%M%S")}.json"), JSON.pretty_generate(email))
242
-
243
- reload_zoho_config!
244
- rule = match_zoho_rule(email)
245
-
246
- if rule
247
- LOG.info "[Zoho] Matched rule: #{rule["label"]}"
248
- if rule["dispatch_agent"]
249
- dispatch_zoho_triage(email, rule)
250
- halt 200, { status: "triage_dispatched", rule: rule["label"], agent: rule["dispatch_agent"] }.to_json
251
- else
252
- notify_zoho_match(email, rule)
253
- halt 200, { status: "matched", rule: rule["label"] }.to_json
254
- end
255
- else
256
- LOG.info "[Zoho] No rules matched"
257
- halt 200, { status: "no_match" }.to_json
258
- end
259
- rescue JSON::ParserError => e
260
- LOG.error "[Zoho] Invalid JSON: #{e.message}"
261
- halt 400, { error: "Invalid JSON" }.to_json
262
- rescue StandardError => e
263
- LOG.error "[Zoho] Unhandled error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
264
- halt 500, { error: e.message }.to_json
265
- end
266
- end
267
-
268
- # --- Zoho OAuth routes ---
269
-
270
- if handler_enabled?("zoho")
271
- ZOHO_AUTH_SCOPES = "ZohoMail.messages.READ,ZohoMail.accounts.READ,ZohoMail.folders.READ".freeze
272
-
273
- get "/zoho/auth" do
274
- reload_zoho_config!
275
- api = ZOHO_CONFIG["api"] || {}
276
- halt 500, "Missing client_id in zoho.json api section" unless api["client_id"]
277
-
278
- redirect_uri = "#{request.base_url}/zoho/callback"
279
- params = URI.encode_www_form(
280
- scope: ZOHO_AUTH_SCOPES,
281
- client_id: api["client_id"],
282
- response_type: "code",
283
- access_type: "offline",
284
- redirect_uri: redirect_uri,
285
- prompt: "consent"
286
- )
287
- redirect "https://accounts.zoho.com/oauth/v2/auth?#{params}"
288
- end
289
-
290
- get "/zoho/callback" do
291
- content_type :html
292
- code = params["code"]
293
- halt 400, "No authorization code received" unless code
294
-
295
- reload_zoho_config!
296
- api = ZOHO_CONFIG["api"] || {}
297
- redirect_uri = "#{request.base_url}/zoho/callback"
298
-
299
- uri = URI(ZOHO_TOKEN_URL)
300
- res = Net::HTTP.post_form(uri, {
301
- "grant_type" => "authorization_code",
302
- "client_id" => api["client_id"],
303
- "client_secret" => api["client_secret"],
304
- "code" => code,
305
- "redirect_uri" => redirect_uri
306
- })
307
-
308
- data = JSON.parse(res.body)
309
- if data["refresh_token"]
310
- ZOHO_CONFIG["api"] ||= {}
311
- ZOHO_CONFIG["api"]["refresh_token"] = data["refresh_token"]
312
- @zoho_access_token = data["access_token"]
313
- @zoho_token_expires_at = Time.now + 3300
314
- File.write(ZOHO_CONFIG_FILE, JSON.pretty_generate(ZOHO_CONFIG))
315
- LOG.info "[Zoho:OAuth] Stored refresh_token and access_token"
316
-
317
- # Auto-fetch account_id if not set
318
- unless ZOHO_CONFIG.dig("api", "account_id")
319
- acct_uri = URI(ZOHO_MAIL_API_BASE.to_s)
320
- http = Net::HTTP.new(acct_uri.host, acct_uri.port)
321
- http.use_ssl = true
322
- req = Net::HTTP::Get.new(acct_uri)
323
- req["Authorization"] = "Zoho-oauthtoken #{@zoho_access_token}"
324
- acct_res = http.request(req)
325
- acct_data = JSON.parse(acct_res.body)
326
- if (account_id = acct_data.dig("data", 0, "accountId"))
327
- ZOHO_CONFIG["api"]["account_id"] = account_id
328
- File.write(ZOHO_CONFIG_FILE, JSON.pretty_generate(ZOHO_CONFIG))
329
- LOG.info "[Zoho:OAuth] Auto-fetched account_id: #{account_id}"
330
- end
331
- end
332
-
333
- "<h1>✅ Zoho OAuth Complete</h1><p>Refresh token and account_id saved to zoho.json. You can close this tab.</p>"
334
- else
335
- LOG.error "[Zoho:OAuth] Token exchange failed: #{data}"
336
- "<h1>❌ OAuth Failed</h1><pre>#{data.to_json}</pre>"
337
- end
338
- rescue StandardError => e
339
- LOG.error "[Zoho:OAuth] Error: #{e.message}"
340
- "<h1>❌ Error</h1><pre>#{e.message}</pre>"
341
- end
342
- end
343
-
344
128
  # --- Admin API routes ---
345
129
  require_relative "lib/brainiac/routes/api"
346
130
 
@@ -364,15 +148,6 @@ get "/dashboard" do
364
148
  erb :dashboard, layout: false
365
149
  end
366
150
 
367
- # --- Discord fallback (plugin handles startup when installed) ---
368
-
369
- unless Brainiac.channel_prompts[:discord]
370
- get "/api/discord" do
371
- content_type :json
372
- { enabled: false, reason: "brainiac-discord plugin not installed" }.to_json
373
- end
374
- end
375
-
376
151
  start_brainiac_restart_monitor
377
152
 
378
153
  LOG.info "[Cron] Starting cron thread..."
@@ -1,6 +1,6 @@
1
1
  {
2
- "galen": {
3
- "display_name": "Galen",
2
+ "sherlock": {
3
+ "display_name": "Sherlock",
4
4
  "local": true,
5
5
  "env": {
6
6
  "DISCORD_BOT_TOKEN": "your-discord-bot-token"
@@ -1,8 +1,3 @@
1
1
  {
2
- "default_agent": "Galen",
3
- "handlers": {
4
- "github": true,
5
- "discord": true,
6
- "zoho": false
7
- }
2
+ "default_agent": "Sherlock"
8
3
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "signing_secret": "YOUR_APPLE_WEBHOOK_SIGNING_SECRET",
3
3
  "discord_channel_id": "CHANNEL_ID_FOR_TESTFLIGHT_NOTIFICATIONS",
4
- "notify_as": "avon",
4
+ "notify_as": "merlin",
5
5
  "api_key_path": "~/.appstoreconnect/AuthKey_6VBGCK56SQ.p8",
6
6
  "api_key_id": "6VBGCK56SQ",
7
7
  "issuer_id": "337a9282-6778-4fe4-bdbb-fd2845ef0c19"
@@ -1,118 +1,56 @@
1
1
  {
2
2
  "users": [
3
3
  {
4
- "canonical_name": "Adam Dalton",
4
+ "canonical_name": "Jane Smith",
5
5
  "identities": {
6
6
  "discord": {
7
- "username": "fladamd",
8
- "user_id": "832331260088287242",
9
- "display_name": "fladamd"
7
+ "username": "janesmith",
8
+ "user_id": "123456789012345678",
9
+ "display_name": "janesmith"
10
10
  },
11
11
  "github": {
12
- "username": "dalton"
13
- },
14
- },
15
- "aliases": ["Andy"],
16
- "relationships": {
17
- "spouse": "Larissa"
18
- },
19
- "notes": "Primary user"
20
- },
21
- {
22
- "canonical_name": "Andy Davis",
23
- "identities": {
24
- "discord": {
25
- "username": "ardavis",
26
- "user_id": "397928984232591361",
27
- "display_name": "ardavis"
28
- },
29
- "github": {
30
- "username": "ardavis"
31
- }
32
- },
33
- "aliases": [],
34
- "relationships": {},
35
- "notes": "Works with Adam on brainiac development"
36
- },
37
- {
38
- "canonical_name": "Kaylee",
39
- "identities": {
40
- "discord": {
41
- "username": "kaylee",
42
- "user_id": "1475961955125825687",
43
- "display_name": "Kaylee"
12
+ "username": "jsmith"
44
13
  }
45
14
  },
46
- "aliases": [],
47
- "relationships": {},
48
- "notes": "AI agent - cheerful, optimistic, loves fixing things"
49
- },
50
- {
51
- "canonical_name": "Sleeper Service",
52
- "identities": {
53
- "discord": {
54
- "username": "sleeper_service",
55
- "user_id": "1476572591220199514",
56
- "display_name": "Sleeper Service"
57
- }
58
- },
59
- "aliases": [],
60
- "relationships": {},
61
- "notes": "AI agent - dry wit, cosmic perspective, understated"
62
- },
63
- {
64
- "canonical_name": "Avon",
65
- "identities": {
66
- "discord": {
67
- "username": "avon",
68
- "user_id": "1476683857922228386",
69
- "display_name": "Avon"
70
- }
71
- },
72
- "aliases": [],
73
- "relationships": {},
74
- "notes": "AI agent - iOS specialist, pragmatic, competence-driven"
15
+ "aliases": ["Jane"],
16
+ "notes": "Primary user"
75
17
  },
76
18
  {
77
- "canonical_name": "Galen",
19
+ "canonical_name": "Sherlock",
78
20
  "identities": {
79
21
  "discord": {
80
- "username": "galen",
81
- "user_id": "1475925968584573181",
82
- "display_name": "Galen"
22
+ "username": "sherlock",
23
+ "user_id": "234567890123456789",
24
+ "display_name": "Sherlock"
83
25
  }
84
26
  },
85
27
  "aliases": [],
86
- "relationships": {},
87
28
  "notes": "AI agent"
88
29
  },
89
30
  {
90
- "canonical_name": "GLaDOS",
31
+ "canonical_name": "Robin",
91
32
  "identities": {
92
33
  "discord": {
93
- "username": "glados",
94
- "user_id": "1475957005817610412",
95
- "display_name": "GLaDOS"
34
+ "username": "robin",
35
+ "user_id": "345678901234567890",
36
+ "display_name": "Robin"
96
37
  }
97
38
  },
98
39
  "aliases": [],
99
- "relationships": {},
100
40
  "notes": "AI agent"
101
41
  },
102
42
  {
103
- "canonical_name": "Threepio",
43
+ "canonical_name": "Merlin",
104
44
  "identities": {
105
45
  "discord": {
106
- "username": "threepio",
107
- "user_id": "1476068144278929438",
108
- "display_name": "Threepio"
46
+ "username": "merlin",
47
+ "user_id": "456789012345678901",
48
+ "display_name": "Merlin"
109
49
  }
110
50
  },
111
51
  "aliases": [],
112
- "relationships": {},
113
52
  "notes": "AI agent"
114
53
  }
115
54
  ],
116
- "schema_version": "1.0",
117
- "last_updated": "2026-02-27T13:52:22-05:00"
55
+ "schema_version": "1.0"
118
56
  }
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.10
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -107,8 +107,9 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '1.25'
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.
110
+ description: 'Core orchestration engine that manages AI agent identity, long-term
111
+ memory (brain), prompt construction, and dispatch. Communication channels are provided
112
+ by plugins: brainiac-discord, brainiac-fizzy, brainiac-github, and more.'
112
113
  executables:
113
114
  - brainiac
114
115
  extensions: []
@@ -128,10 +129,8 @@ files:
128
129
  - lib/brainiac/brain.rb
129
130
  - lib/brainiac/config.rb
130
131
  - lib/brainiac/cron.rb
131
- - lib/brainiac/handlers/github.rb
132
132
  - lib/brainiac/handlers/shared/git.rb
133
133
  - lib/brainiac/handlers/shared/inline_tags.rb
134
- - lib/brainiac/handlers/zoho.rb
135
134
  - lib/brainiac/helpers.rb
136
135
  - lib/brainiac/hooks.rb
137
136
  - lib/brainiac/notifications.rb
@@ -143,7 +142,6 @@ files:
143
142
  - lib/brainiac/skills.rb
144
143
  - lib/brainiac/users.rb
145
144
  - lib/brainiac/version.rb
146
- - lib/brainiac/zoho_mail_api.rb
147
145
  - monitor/daemon.rb
148
146
  - monitor/shared.rb
149
147
  - monitor/waybar/deploy_env.rb
@@ -161,14 +159,12 @@ files:
161
159
  - templates/brainiac.json.example
162
160
  - templates/cli-providers/grok.json.example
163
161
  - templates/cli-providers/kiro.json.example
164
- - templates/github.json.example
165
162
  - templates/plugins.json.example
166
163
  - templates/roles/code-reviewer.md.example
167
164
  - templates/roles/general-engineer.md.example
168
165
  - templates/roles/test-engineer.md.example
169
166
  - templates/testflight.json.example
170
167
  - templates/users.json.example
171
- - templates/zoho.json.example
172
168
  - views/dashboard.erb
173
169
  homepage: https://github.com/stowzilla/brainiac
174
170
  licenses:
@@ -191,5 +187,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
187
  requirements: []
192
188
  rubygems_version: 3.6.9
193
189
  specification_version: 4
194
- summary: AI agent webhook receiver and dispatcher
190
+ summary: Multi-agent orchestration layer for developer workflows
195
191
  test_files: []