brainiac 0.0.6 → 0.0.7

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +382 -11
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +15 -38
  7. data/lib/brainiac/config.rb +40 -11
  8. data/lib/brainiac/handlers/discord/api.rb +196 -0
  9. data/lib/brainiac/handlers/discord/config.rb +134 -0
  10. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  11. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  12. data/lib/brainiac/handlers/discord/message.rb +933 -0
  13. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  14. data/lib/brainiac/handlers/discord.rb +14 -1892
  15. data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
  16. data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
  17. data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
  18. data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
  19. data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
  20. data/lib/brainiac/handlers/fizzy.rb +12 -1290
  21. data/lib/brainiac/handlers/github.rb +149 -212
  22. data/lib/brainiac/handlers/shared/git.rb +203 -0
  23. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  24. data/lib/brainiac/handlers/zoho.rb +79 -76
  25. data/lib/brainiac/helpers.rb +2 -121
  26. data/lib/brainiac/planning.rb +2 -2
  27. data/lib/brainiac/plugins.rb +129 -0
  28. data/lib/brainiac/restart.rb +94 -0
  29. data/lib/brainiac/routes/api.rb +427 -0
  30. data/lib/brainiac/version.rb +1 -1
  31. data/lib/brainiac/zoho_mail_api.rb +2 -1
  32. data/lib/brainiac.rb +7 -0
  33. data/monitor/daemon.rb +4 -27
  34. data/monitor/shared.rb +247 -0
  35. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
  36. data/monitor/waybar/setup.rb +232 -0
  37. data/monitor/waybar/status.rb +51 -0
  38. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  39. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  40. data/monitor/xbar/plugin.rb +155 -0
  41. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  42. data/receiver.rb +91 -450
  43. data/templates/brainiac.json.example +9 -0
  44. data/templates/cli-providers/kiro.json.example +8 -2
  45. data/templates/plugins.json.example +3 -0
  46. metadata +30 -20
  47. data/lib/user_registry.rb +0 -159
  48. data/monitor/menubar.rb +0 -295
  49. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  50. data/monitor/setup-waybar-deployments.rb +0 -96
  51. data/monitor/setup-waybar-module.rb +0 -113
  52. data/monitor/setup-xbar-plugin.rb +0 -35
  53. data/monitor/view-logs.rb +0 -119
  54. data/monitor/waybar-config-updater.rb +0 -56
  55. data/monitor/waybar-deployments.rb +0 -239
  56. data/monitor/waybar.rb +0 -146
  57. data/monitor/xbar.3s.rb +0 -179
  58. /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
  59. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  60. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
data/bin/brainiac CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require "English"
3
4
  require "json"
4
5
  require "optparse"
5
6
  require "fileutils"
@@ -49,6 +50,21 @@ def save_config(config)
49
50
  File.write(CONFIG_FILE, JSON.pretty_generate(config))
50
51
  end
51
52
 
53
+ # Resolve the default agent name: env var → brainiac.json → nil.
54
+ def default_agent_name
55
+ return ENV["AI_AGENT_NAME"] if ENV["AI_AGENT_NAME"] && !ENV["AI_AGENT_NAME"].empty?
56
+
57
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
58
+ if File.exist?(brainiac_config_path)
59
+ config = JSON.parse(File.read(brainiac_config_path))
60
+ return config["default_agent"] if config["default_agent"]
61
+ end
62
+
63
+ nil
64
+ rescue JSON::ParserError
65
+ nil
66
+ end
67
+
52
68
  def find_server_pid
53
69
  # First check PID file
54
70
  if File.exist?(PID_FILE)
@@ -187,6 +203,23 @@ def stop_server
187
203
  end
188
204
  end
189
205
 
206
+ def update_handler_config(name, enabled)
207
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
208
+ config = if File.exist?(brainiac_config_path)
209
+ JSON.parse(File.read(brainiac_config_path))
210
+ else
211
+ {}
212
+ end
213
+
214
+ config["handlers"] ||= {}
215
+ config["handlers"][name] = enabled
216
+ File.write(brainiac_config_path, JSON.pretty_generate(config))
217
+
218
+ action = enabled ? "enabled" : "disabled"
219
+ puts "✓ Handler '#{name}' #{action}"
220
+ puts " Restart the server for changes to take effect: brainiac restart"
221
+ end
222
+
190
223
  def start_server(daemon: false)
191
224
  # Resolve the real path of the brainiac script (follows symlinks)
192
225
  receiver_path = File.join(BRAINIAC_ROOT, "receiver.rb")
@@ -310,10 +343,19 @@ def register_project(options)
310
343
  agent_model = $stdin.gets.chomp
311
344
  agent_model = "auto" if agent_model.empty?
312
345
 
313
- default_agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
314
- print "Agent name [#{default_agent_name}]: "
315
- agent_name = $stdin.gets.chomp
316
- agent_name = default_agent_name if agent_name.empty?
346
+ agent_default = default_agent_name
347
+ if agent_default
348
+ print "Agent name [#{agent_default}]: "
349
+ agent_name = $stdin.gets.chomp
350
+ agent_name = agent_default if agent_name.empty?
351
+ else
352
+ print "Agent name: "
353
+ agent_name = $stdin.gets.chomp
354
+ if agent_name.empty?
355
+ puts "Error: No default agent configured. Provide a name or run 'brainiac agent default <name>' first."
356
+ exit 1
357
+ end
358
+ end
317
359
 
318
360
  # Build project config
319
361
  project_config = {
@@ -524,7 +566,7 @@ def brain_search(query, scope: :knowledge, agent_name: nil)
524
566
 
525
567
  collection = case scope
526
568
  when :persona
527
- agent_name ||= ENV.fetch("AI_AGENT_NAME", "Galen")
569
+ agent_name ||= default_agent_name || "agent"
528
570
  persona_collection_for(agent_name)
529
571
  else
530
572
  KNOWLEDGE_COLLECTION
@@ -611,6 +653,52 @@ when "setup"
611
653
  puts "✓ Copied #{copied} example config(s) to ~/.brainiac/ (existing files preserved)"
612
654
  end
613
655
 
656
+ # Copy CLI provider examples
657
+ providers_dir = File.join(BRAINIAC_DIR, "cli-providers")
658
+ FileUtils.mkdir_p(providers_dir)
659
+ providers_src = File.join(BRAINIAC_ROOT, "templates", "cli-providers")
660
+ if Dir.exist?(providers_src)
661
+ Dir.glob(File.join(providers_src, "*.example")).each do |template|
662
+ dest = File.join(providers_dir, File.basename(template, ".example"))
663
+ FileUtils.cp(template, dest) unless File.exist?(dest)
664
+ end
665
+ end
666
+
667
+ # Handler configuration
668
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
669
+ puts ""
670
+ if File.exist?(brainiac_config_path)
671
+ puts "✓ Handler config already exists at ~/.brainiac/brainiac.json"
672
+ else
673
+ puts "Which integrations do you want to enable?"
674
+ puts "(You can change this later in ~/.brainiac/brainiac.json)"
675
+ puts ""
676
+
677
+ available_handlers = {
678
+ "fizzy" => "Fizzy (card management, agent dispatch via webhooks)",
679
+ "github" => "GitHub (PR reviews, CI failure handling, deploy tracking)",
680
+ "discord" => "Discord (conversational agent access via bot)",
681
+ "zoho" => "Zoho Mail (email notifications and triage)"
682
+ }
683
+
684
+ enabled_handlers = {}
685
+ available_handlers.each do |key, description|
686
+ print " Enable #{description}? [Y/n] "
687
+ answer = $stdin.gets&.strip&.downcase
688
+ enabled_handlers[key] = answer != "n"
689
+ end
690
+
691
+ config = { "handlers" => enabled_handlers }
692
+ File.write(brainiac_config_path, JSON.pretty_generate(config))
693
+ puts ""
694
+ puts "✓ Wrote handler config to ~/.brainiac/brainiac.json"
695
+
696
+ enabled_list = enabled_handlers.select { |_, v| v }.keys
697
+ disabled_list = enabled_handlers.reject { |_, v| v }.keys
698
+ puts " Enabled: #{enabled_list.join(", ")}" if enabled_list.any?
699
+ puts " Disabled: #{disabled_list.join(", ")}" if disabled_list.any?
700
+ end
701
+
614
702
  puts ""
615
703
  puts "Next steps:"
616
704
  puts " 1. Edit config files in ~/.brainiac/ with your secrets and IDs"
@@ -684,6 +772,64 @@ when "show"
684
772
  end
685
773
  show_project(ARGV[0])
686
774
 
775
+ when "handler", "handlers"
776
+ handler_cmd = ARGV.shift
777
+ case handler_cmd
778
+ when "list", "ls", nil
779
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
780
+ config = if File.exist?(brainiac_config_path)
781
+ JSON.parse(File.read(brainiac_config_path))
782
+ else
783
+ {}
784
+ end
785
+ handlers = config["handlers"] || {}
786
+
787
+ # Built-in handlers
788
+ builtins = %w[fizzy github discord zoho]
789
+ puts "Built-in handlers:"
790
+ builtins.each do |name|
791
+ enabled = handlers.fetch(name, true)
792
+ status = enabled ? "✓ enabled" : "✗ disabled"
793
+ puts " #{name.ljust(10)} #{status}"
794
+ end
795
+
796
+ # Custom handlers
797
+ custom_dir = File.join(BRAINIAC_DIR, "handlers")
798
+ if Dir.exist?(custom_dir)
799
+ custom_files = Dir.glob(File.join(custom_dir, "*.rb"))
800
+ if custom_files.any?
801
+ puts ""
802
+ puts "Custom handlers (#{custom_dir}):"
803
+ custom_files.each do |f|
804
+ name = File.basename(f, ".rb")
805
+ enabled = handlers.fetch(name, true)
806
+ status = enabled ? "✓ enabled" : "✗ disabled"
807
+ puts " #{name.ljust(10)} #{status}"
808
+ end
809
+ end
810
+ end
811
+
812
+ when "enable"
813
+ name = ARGV.shift
814
+ unless name
815
+ puts "Usage: brainiac handler enable <name>"
816
+ exit 1
817
+ end
818
+ update_handler_config(name, true)
819
+
820
+ when "disable"
821
+ name = ARGV.shift
822
+ unless name
823
+ puts "Usage: brainiac handler disable <name>"
824
+ exit 1
825
+ end
826
+ update_handler_config(name, false)
827
+
828
+ else
829
+ puts "Usage: brainiac handler <list|enable|disable> [name]"
830
+ exit 1
831
+ end
832
+
687
833
  when "config"
688
834
  OptionParser.new do |opts|
689
835
  opts.banner = "Usage: brainiac config [options]"
@@ -952,14 +1098,24 @@ when "brain"
952
1098
  brain_cmd = ARGV.shift
953
1099
  case brain_cmd
954
1100
  when "init"
955
- agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
1101
+ agent = ARGV[0] || default_agent_name
1102
+ unless agent
1103
+ puts "Usage: brainiac brain init <agent-name>"
1104
+ puts " Or set a default: brainiac agent default <name>"
1105
+ exit 1
1106
+ end
956
1107
  brain_init(agent)
957
1108
  when "status", "show"
958
- agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
1109
+ agent = ARGV[0] || default_agent_name
1110
+ unless agent
1111
+ puts "Usage: brainiac brain status <agent-name>"
1112
+ puts " Or set a default: brainiac agent default <name>"
1113
+ exit 1
1114
+ end
959
1115
  brain_status(agent)
960
1116
  when "search", "query"
961
1117
  scope = :knowledge
962
- agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
1118
+ agent_name = default_agent_name
963
1119
  if ARGV.include?("--persona")
964
1120
  ARGV.delete("--persona")
965
1121
  scope = :persona
@@ -979,7 +1135,11 @@ when "brain"
979
1135
  when "path"
980
1136
  what = ARGV[0]
981
1137
  if what == "persona"
982
- agent = ARGV[1] || ENV.fetch("AI_AGENT_NAME", "Galen")
1138
+ agent = ARGV[1] || default_agent_name
1139
+ unless agent
1140
+ puts "Usage: brainiac brain path persona <agent-name>"
1141
+ exit 1
1142
+ end
983
1143
  puts persona_dir_for(agent)
984
1144
  else
985
1145
  puts KNOWLEDGE_DIR
@@ -1021,7 +1181,7 @@ when "cron"
1021
1181
  when "add", "create"
1022
1182
  # Parse arguments
1023
1183
  schedule = nil
1024
- agent = ENV.fetch("AI_AGENT_NAME", "Galen")
1184
+ agent = default_agent_name
1025
1185
  project = nil
1026
1186
  model = nil
1027
1187
  effort = nil
@@ -1402,6 +1562,10 @@ when "agent"
1402
1562
  case agent_cmd
1403
1563
  when "list", "ls"
1404
1564
  registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
1565
+
1566
+ # Determine current default agent
1567
+ default_agent = default_agent_name
1568
+
1405
1569
  if registry.empty?
1406
1570
  puts "No agents in registry."
1407
1571
  else
@@ -1414,13 +1578,15 @@ when "agent"
1414
1578
  end
1415
1579
  details << "cli:#{entry["cli_provider"]}" if entry["cli_provider"]
1416
1580
  details << "env:#{entry["env"].size}" if entry["env"]&.any?
1417
- { key: key, display: display, local: entry["local"], details: details.join(" | ") }
1581
+ is_default = default_agent && display.downcase == default_agent.downcase
1582
+ { key: key, display: display, local: entry["local"], details: details.join(" | "), default: is_default }
1418
1583
  end
1419
1584
 
1420
1585
  local, remote = agents.partition { |a| a[:local] }
1421
1586
 
1422
1587
  local.each do |a|
1423
1588
  line = " #{a[:display]} (#{a[:key]})"
1589
+ line += " ★ default" if a[:default]
1424
1590
  line += " #{a[:details]}" unless a[:details].empty?
1425
1591
  puts line
1426
1592
  end
@@ -1430,12 +1596,51 @@ when "agent"
1430
1596
  puts " Remote:" if local.any?
1431
1597
  remote.each do |a|
1432
1598
  line = " #{a[:display]} (#{a[:key]})"
1599
+ line += " ★ default" if a[:default]
1433
1600
  line += " #{a[:details]}" unless a[:details].empty?
1434
1601
  puts line
1435
1602
  end
1436
1603
  end
1437
1604
  end
1438
1605
 
1606
+ when "default"
1607
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
1608
+ brainiac_config = File.exist?(brainiac_config_path) ? JSON.parse(File.read(brainiac_config_path)) : {}
1609
+ name = ARGV.shift
1610
+
1611
+ if name
1612
+ # Set default agent
1613
+ registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
1614
+ agent_key = name.downcase.gsub(/[^a-z0-9-]/, "-")
1615
+
1616
+ # Resolve display name from registry if it exists
1617
+ display_name = if registry.dig(agent_key, "fizzy_name")
1618
+ registry[agent_key]["fizzy_name"]
1619
+ else
1620
+ name
1621
+ end
1622
+
1623
+ brainiac_config["default_agent"] = display_name
1624
+ ensure_brainiac_dir
1625
+ File.write(brainiac_config_path, JSON.pretty_generate(brainiac_config))
1626
+ puts "✓ Default agent set to '#{display_name}'"
1627
+ else
1628
+ # Show current default
1629
+ env_override = ENV.fetch("AI_AGENT_NAME", nil)
1630
+ config_default = brainiac_config["default_agent"]
1631
+
1632
+ if env_override
1633
+ puts "Default agent: #{env_override} (from AI_AGENT_NAME env var)"
1634
+ puts " Config value: #{config_default}" if config_default && config_default != env_override
1635
+ elsif config_default
1636
+ puts "Default agent: #{config_default}"
1637
+ else
1638
+ puts "No default agent configured."
1639
+ puts "Set one with: brainiac agent default <name>"
1640
+ puts " Or set AI_AGENT_NAME in your environment."
1641
+ end
1642
+ end
1643
+
1439
1644
  when "create", "add"
1440
1645
  name = ARGV.shift
1441
1646
  unless name
@@ -1581,11 +1786,13 @@ when "agent"
1581
1786
  puts <<~HELP
1582
1787
  Usage: brainiac agent <name> <command> [args]
1583
1788
  brainiac agent list
1789
+ brainiac agent default [name]
1584
1790
  brainiac agent create <name> [options]
1585
1791
  brainiac agent remove <name>
1586
1792
 
1587
1793
  Commands:
1588
1794
  list List all agents in the registry
1795
+ default [name] Show or set the default agent
1589
1796
  create <name> [options] Add a new agent to the registry
1590
1797
  remove <name> Remove an agent from the registry
1591
1798
  <name> show Show agent configuration (tokens redacted)
@@ -1601,6 +1808,8 @@ when "agent"
1601
1808
  --persona <text> Create a persona style file
1602
1809
 
1603
1810
  Examples:
1811
+ brainiac agent list
1812
+ brainiac agent default Galen
1604
1813
  brainiac agent create SecurityBot --local --role code-reviewer
1605
1814
  brainiac agent create Jane --cli grok
1606
1815
  brainiac agent create MyAgent # interactive mode
@@ -1936,6 +2145,160 @@ when "completions"
1936
2145
  puts File.read(completion_file)
1937
2146
  end
1938
2147
 
2148
+ when "install"
2149
+ name = ARGV.shift
2150
+ unless name
2151
+ puts "Usage: brainiac install <plugin-name>"
2152
+ puts ""
2153
+ puts "Installs a Brainiac plugin gem (brainiac-<name>) and registers it."
2154
+ puts ""
2155
+ puts "Examples:"
2156
+ puts " brainiac install whatsapp # installs brainiac-whatsapp gem"
2157
+ puts " brainiac install slack # installs brainiac-slack gem"
2158
+ puts ""
2159
+ puts "After installing, restart the server: brainiac restart"
2160
+ exit 1
2161
+ end
2162
+
2163
+ # Strip "brainiac-" prefix if user includes it
2164
+ name = name.sub(/^brainiac-/, "")
2165
+ version = nil
2166
+
2167
+ # Check for --version flag
2168
+ if ["--version", "-v"].include?(ARGV[0])
2169
+ ARGV.shift
2170
+ version = ARGV.shift
2171
+ end
2172
+
2173
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2174
+ plugins_config = if File.exist?(plugins_file)
2175
+ JSON.parse(File.read(plugins_file))
2176
+ else
2177
+ { "plugins" => [] }
2178
+ end
2179
+
2180
+ existing = (plugins_config["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2181
+ if existing
2182
+ puts "Plugin '#{name}' is already installed."
2183
+ puts " To reinstall: brainiac uninstall #{name} && brainiac install #{name}"
2184
+ exit 0
2185
+ end
2186
+
2187
+ gem_name = "brainiac-#{name}"
2188
+ puts "Installing #{gem_name}..."
2189
+
2190
+ install_cmd = "gem install #{gem_name}"
2191
+ install_cmd += " --version '#{version}'" if version
2192
+
2193
+ system(install_cmd)
2194
+ unless $CHILD_STATUS.success?
2195
+ puts ""
2196
+ puts "Failed to install #{gem_name}."
2197
+ puts " Check that the gem exists: https://rubygems.org/gems/#{gem_name}"
2198
+ exit 1
2199
+ end
2200
+
2201
+ # Register in plugins.json
2202
+ plugins_config["plugins"] ||= []
2203
+ entry = { "name" => name, "gem" => gem_name, "installed_at" => Time.now.iso8601 }
2204
+ entry["version"] = version if version
2205
+ plugins_config["plugins"] << entry
2206
+
2207
+ ensure_brainiac_dir
2208
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
2209
+
2210
+ # Enable the handler
2211
+ update_handler_config(name, true)
2212
+
2213
+ puts ""
2214
+ puts "✓ Installed plugin '#{name}' (#{gem_name})"
2215
+ puts " Restart the server to activate: brainiac restart"
2216
+
2217
+ when "uninstall"
2218
+ name = ARGV.shift
2219
+ unless name
2220
+ puts "Usage: brainiac uninstall <plugin-name>"
2221
+ puts ""
2222
+ puts "Removes a Brainiac plugin from the configuration."
2223
+ puts "The gem itself remains installed (use 'gem uninstall brainiac-<name>' to fully remove)."
2224
+ exit 1
2225
+ end
2226
+
2227
+ name = name.sub(/^brainiac-/, "")
2228
+ gem_name = "brainiac-#{name}"
2229
+
2230
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2231
+ unless File.exist?(plugins_file)
2232
+ puts "Plugin '#{name}' is not installed (no plugins.json found)."
2233
+ exit 1
2234
+ end
2235
+
2236
+ plugins_config = JSON.parse(File.read(plugins_file))
2237
+ found = (plugins_config["plugins"] || []).any? { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2238
+
2239
+ unless found
2240
+ puts "Plugin '#{name}' is not installed."
2241
+ exit 1
2242
+ end
2243
+
2244
+ plugins_config["plugins"].reject! { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2245
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
2246
+
2247
+ # Disable the handler
2248
+ update_handler_config(name, false)
2249
+
2250
+ puts "✓ Removed plugin '#{name}' from Brainiac."
2251
+ puts " The gem #{gem_name} is still installed system-wide."
2252
+ puts " To fully remove: gem uninstall #{gem_name}"
2253
+ puts " Restart the server to apply: brainiac restart"
2254
+
2255
+ when "plugins"
2256
+ plugins_cmd = ARGV.shift
2257
+
2258
+ case plugins_cmd
2259
+ when "list", "ls", nil
2260
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2261
+ plugins_config = if File.exist?(plugins_file)
2262
+ JSON.parse(File.read(plugins_file))
2263
+ else
2264
+ { "plugins" => [] }
2265
+ end
2266
+
2267
+ plugins = plugins_config["plugins"] || []
2268
+ if plugins.empty?
2269
+ puts "No plugins installed."
2270
+ puts ""
2271
+ puts "Install plugins with: brainiac install <name>"
2272
+ puts " Example: brainiac install whatsapp"
2273
+ else
2274
+ puts "Installed plugins:"
2275
+ plugins.each do |p|
2276
+ entry = p.is_a?(Hash) ? p : { "name" => p.to_s }
2277
+ name = entry["name"]
2278
+ gem_name = entry["gem"] || "brainiac-#{name}"
2279
+ version = entry["version"] || "latest"
2280
+ installed_at = entry["installed_at"] ? " (#{entry["installed_at"][0..9]})" : ""
2281
+
2282
+ # Check if gem is actually loadable
2283
+ loadable = begin
2284
+ Gem::Specification.find_by_name(gem_name)
2285
+ "✓"
2286
+ rescue Gem::MissingSpecError
2287
+ "✗ (gem not found)"
2288
+ end
2289
+
2290
+ puts " #{loadable} #{name} (#{gem_name} #{version})#{installed_at}"
2291
+ end
2292
+ end
2293
+ else
2294
+ puts "Usage: brainiac plugins [list]"
2295
+ puts ""
2296
+ puts "Managing plugins:"
2297
+ puts " brainiac install <name> Install a plugin"
2298
+ puts " brainiac uninstall <name> Remove a plugin"
2299
+ puts " brainiac plugins List installed plugins"
2300
+ end
2301
+
1939
2302
  when "version", "--version", "-v"
1940
2303
  puts "brainiac #{BRAINIAC_VERSION}"
1941
2304
 
@@ -1956,6 +2319,9 @@ when "help", "--help", "-h", nil
1956
2319
  brainiac projects list List all registered projects (alias)
1957
2320
  brainiac projects default <key> Set the default project
1958
2321
  brainiac show <key> Show project configuration
2322
+ brainiac install <plugin> Install a Brainiac plugin (gem-based handler)
2323
+ brainiac uninstall <plugin> Remove an installed plugin
2324
+ brainiac plugins List installed plugins
1959
2325
  brainiac brain <command> Manage agent long-term memory (brain)
1960
2326
  brainiac discord <command> Manage the Discord bot
1961
2327
  brainiac cron <command> Manage scheduled agent tasks
@@ -1970,6 +2336,11 @@ when "help", "--help", "-h", nil
1970
2336
  Server Options:
1971
2337
  -d, --daemon Run server in background (detached)
1972
2338
 
2339
+ Plugin Commands:
2340
+ install <name> Install a plugin (gem install brainiac-<name>)
2341
+ uninstall <name> Remove a plugin
2342
+ plugins List installed plugins
2343
+
1973
2344
  Card Map Commands:
1974
2345
  card-map list Show all card map entries
1975
2346
  card-map clean Remove stale entries (missing worktrees)
@@ -9,7 +9,7 @@ _brainiac() {
9
9
  local brainiac_dir="${BRAINIAC_DIR:-$HOME/.brainiac}"
10
10
 
11
11
  # Top-level commands
12
- local commands="server stop restart logs status register unregister list show brain discord cron provider role agent config path version help setup projects card-map"
12
+ local commands="server stop restart logs status register unregister list show brain discord cron provider role agent config path version help setup projects card-map handler"
13
13
 
14
14
  # Helper: list agent keys from registry
15
15
  _brainiac_agents() {
@@ -20,49 +20,26 @@
20
20
  # (card assignments). Agents without "local": true are still known for
21
21
  # mention detection, display names, tokens, and cross-agent interactions —
22
22
  # they just won't pick up card assignments on this machine.
23
- #
24
- # Legacy format with top-level `fizzy_token` / `discord_bot_token` keys is
25
- # auto-migrated into the `env` hash at load time.
26
-
27
23
  def load_agent_registry
28
- if File.exist?(AGENT_REGISTRY_FILE)
29
- raw_registry = JSON.parse(File.read(AGENT_REGISTRY_FILE))
30
- LOG.info "Loaded agent registry (#{raw_registry.size} agents) from #{AGENT_REGISTRY_FILE}"
31
-
32
- # Normalize keys: convert to lowercase, replace non-alphanumeric with hyphens
33
- registry = {}
34
- raw_registry.each do |key, entry|
35
- normalized_key = key.downcase.gsub(/[^a-z0-9-]/, "-")
36
- if registry.key?(normalized_key) && registry[normalized_key] != entry
37
- LOG.warn "Duplicate agent key after normalization: '#{key}' → '#{normalized_key}' (already exists)"
38
- end
39
- registry[normalized_key] = entry
40
- end
41
-
42
- # Migrate legacy keys into env hash
43
- registry.each_value do |entry|
44
- next unless entry.is_a?(Hash)
45
-
46
- entry["env"] ||= {}
47
- # Migrate fizzy_token → FIZZY_TOKEN
48
- if (ft = entry.delete("fizzy_token"))
49
- entry["env"]["FIZZY_TOKEN"] ||= ft
50
- end
51
- # Migrate discord_bot_token → DISCORD_BOT_TOKEN
52
- if (dt = entry.delete("discord_bot_token"))
53
- entry["env"]["DISCORD_BOT_TOKEN"] ||= dt
54
- end
55
- end
56
- return registry
24
+ unless File.exist?(AGENT_REGISTRY_FILE)
25
+ LOG.info "No agent registry found at #{AGENT_REGISTRY_FILE}"
26
+ return {}
57
27
  end
58
28
 
59
- if File.exist?(AGENT_TOKENS_FILE)
60
- tokens = JSON.parse(File.read(AGENT_TOKENS_FILE))
61
- LOG.info "Loaded legacy agent tokens (#{tokens.size} agents) from #{AGENT_TOKENS_FILE}"
62
- return tokens.transform_values { |token| { "env" => { "FIZZY_TOKEN" => token } } }
29
+ raw_registry = JSON.parse(File.read(AGENT_REGISTRY_FILE))
30
+ LOG.info "Loaded agent registry (#{raw_registry.size} agents) from #{AGENT_REGISTRY_FILE}"
31
+
32
+ # Normalize keys: convert to lowercase, replace non-alphanumeric with hyphens
33
+ registry = {}
34
+ raw_registry.each do |key, entry|
35
+ normalized_key = key.downcase.gsub(/[^a-z0-9-]/, "-")
36
+ if registry.key?(normalized_key) && registry[normalized_key] != entry
37
+ LOG.warn "Duplicate agent key after normalization: '#{key}' → '#{normalized_key}' (already exists)"
38
+ end
39
+ registry[normalized_key] = entry
63
40
  end
64
41
 
65
- {}
42
+ registry
66
43
  rescue JSON::ParserError => e
67
44
  LOG.error "Failed to parse agent registry: #{e.message}"
68
45
  {}
@@ -16,20 +16,47 @@ BRAINIAC_VERSION = Brainiac::VERSION
16
16
  # --- Environment & paths ---
17
17
 
18
18
  FIZZY_WEBHOOK_SECRET = ENV.fetch("FIZZY_WEBHOOK_SECRET", nil)
19
- AI_AGENT_NAME = ENV.fetch("AI_AGENT_NAME") do
20
- case RbConfig::CONFIG["host_os"]
21
- when /darwin/i then "Kaylee"
22
- else "Galen"
23
- end
24
- end
25
19
 
26
20
  BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
27
21
  PROJECTS_FILE = File.join(BRAINIAC_DIR, "projects.json")
28
22
  KIRO_AGENTS_DIR = File.join(Dir.home, ".kiro", "agents")
29
23
  CARD_MAP_FILE = File.join(BRAINIAC_DIR, "card_map.json")
30
- AGENT_TOKENS_FILE = File.join(BRAINIAC_DIR, "agent_tokens.json")
31
24
  AGENT_REGISTRY_FILE = File.join(BRAINIAC_DIR, "agents.json")
32
25
 
26
+ # --- Master config (handler toggles, global settings) ---
27
+
28
+ BRAINIAC_CONFIG_FILE = File.join(BRAINIAC_DIR, "brainiac.json")
29
+
30
+ def load_brainiac_config
31
+ return {} unless File.exist?(BRAINIAC_CONFIG_FILE)
32
+
33
+ JSON.parse(File.read(BRAINIAC_CONFIG_FILE))
34
+ rescue JSON::ParserError => e
35
+ LOG.error "Failed to parse brainiac.json: #{e.message}" if defined?(LOG)
36
+ {}
37
+ end
38
+
39
+ BRAINIAC_CONFIG = load_brainiac_config
40
+
41
+ # --- Default agent name ---
42
+ # Priority: AI_AGENT_NAME env var → brainiac.json "default_agent" → error.
43
+ AI_AGENT_NAME = ENV.fetch("AI_AGENT_NAME", nil) || BRAINIAC_CONFIG["default_agent"] || begin
44
+ raise <<~MSG
45
+ No default agent configured. Set one of:
46
+ 1. Environment variable: export AI_AGENT_NAME="YourAgent"
47
+ 2. In ~/.brainiac/brainiac.json: { "default_agent": "YourAgent" }
48
+ MSG
49
+ end
50
+
51
+ # Check whether a handler is enabled. Defaults to true when brainiac.json
52
+ # doesn't exist or doesn't list the handler (backwards compatible — all on).
53
+ def handler_enabled?(name)
54
+ handlers = BRAINIAC_CONFIG["handlers"]
55
+ return true unless handlers # No config = everything enabled
56
+
57
+ handlers.fetch(name.to_s, true)
58
+ end
59
+
33
60
  LOG_LEVEL = ENV.fetch("LOG_LEVEL", "info").downcase
34
61
  LOG = Logger.new($stdout)
35
62
  LOG.level = case LOG_LEVEL
@@ -84,7 +111,7 @@ end
84
111
  GITHUB_CONFIG = load_github_config
85
112
 
86
113
  def github_webhook_secret
87
- # Fallback to env var for backwards compatibility
114
+ # Fallback to env var
88
115
  GITHUB_CONFIG["webhook_secret"] || ENV.fetch("GITHUB_WEBHOOK_SECRET", nil)
89
116
  end
90
117
 
@@ -187,8 +214,11 @@ DEFAULT_PROJECT = {
187
214
  "repo_path" => ENV.fetch("REPO_PATH", Dir.pwd),
188
215
  "fizzy_tags" => [],
189
216
  "github_repo" => ENV.fetch("GITHUB_REPO", nil),
217
+ # CLI defaults below are overridden by ~/.brainiac/cli-providers/*.json when a
218
+ # cli_provider is configured on the project or agent. These only apply as a
219
+ # last-resort fallback when no provider config exists.
190
220
  "agent_cli" => ENV.fetch("AGENT_CLI", "kiro-cli"),
191
- "agent_cli_args" => ENV.fetch("AGENT_CLI_ARGS", "chat --trust-all-tools --no-interactive"),
221
+ "agent_cli_args" => ENV.fetch("AGENT_CLI_ARGS", "chat --no-interactive"),
192
222
  "agent_model_flag" => ENV["AGENT_MODEL_FLAG"] || "--model",
193
223
  "agent_model" => ENV.fetch("AGENT_MODEL", nil),
194
224
  "agent_effort_flag" => ENV["AGENT_EFFORT_FLAG"] || "--effort",
@@ -208,8 +238,7 @@ DEFAULT_PROJECT = {
208
238
  }.freeze
209
239
 
210
240
  # --- Discord (optional) ---
211
- # Discord is enabled when any agent in the registry has a discord_bot_token,
212
- # OR when the legacy DISCORD_BOT_TOKEN env var is set.
241
+ # Discord is enabled when any agent in the registry has a discord_bot_token
213
242
  # Requires the websocket-client-simple gem.
214
243
 
215
244
  DISCORD_ENABLED = begin