brainiac 0.0.6 → 0.0.8

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +472 -44
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +27 -74
  7. data/lib/brainiac/brain.rb +6 -6
  8. data/lib/brainiac/config.rb +40 -76
  9. data/lib/brainiac/handlers/discord/api.rb +196 -0
  10. data/lib/brainiac/handlers/discord/config.rb +134 -0
  11. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  12. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  13. data/lib/brainiac/handlers/discord/message.rb +933 -0
  14. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  15. data/lib/brainiac/handlers/discord.rb +14 -1892
  16. data/lib/brainiac/handlers/github.rb +134 -317
  17. data/lib/brainiac/handlers/shared/git.rb +190 -0
  18. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  19. data/lib/brainiac/handlers/zoho.rb +103 -153
  20. data/lib/brainiac/helpers.rb +43 -455
  21. data/lib/brainiac/hooks.rb +86 -0
  22. data/lib/brainiac/plugins.rb +154 -0
  23. data/lib/brainiac/prompts.rb +34 -172
  24. data/lib/brainiac/restart.rb +112 -0
  25. data/lib/brainiac/routes/api.rb +411 -0
  26. data/lib/brainiac/users.rb +1 -7
  27. data/lib/brainiac/version.rb +1 -1
  28. data/lib/brainiac/zoho_mail_api.rb +2 -1
  29. data/lib/brainiac.rb +8 -1
  30. data/monitor/daemon.rb +4 -27
  31. data/monitor/shared.rb +247 -0
  32. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +17 -89
  33. data/monitor/waybar/setup.rb +232 -0
  34. data/monitor/waybar/status.rb +51 -0
  35. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  36. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  37. data/monitor/xbar/plugin.rb +149 -0
  38. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  39. data/receiver.rb +44 -551
  40. data/templates/agents.json.example +1 -2
  41. data/templates/brainiac.json.example +8 -0
  42. data/templates/cli-providers/kiro.json.example +8 -2
  43. data/templates/plugins.json.example +3 -0
  44. data/templates/users.json.example +0 -3
  45. metadata +25 -23
  46. data/lib/brainiac/card_index.rb +0 -389
  47. data/lib/brainiac/deployments.rb +0 -258
  48. data/lib/brainiac/handlers/fizzy.rb +0 -1292
  49. data/lib/brainiac/planning.rb +0 -237
  50. data/lib/user_registry.rb +0 -159
  51. data/monitor/menubar.rb +0 -295
  52. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  53. data/monitor/setup-waybar-deployments.rb +0 -96
  54. data/monitor/setup-waybar-module.rb +0 -113
  55. data/monitor/setup-xbar-plugin.rb +0 -35
  56. data/monitor/view-logs.rb +0 -119
  57. data/monitor/waybar-config-updater.rb +0 -56
  58. data/monitor/waybar-deployments.rb +0 -239
  59. data/monitor/waybar.rb +0 -146
  60. data/monitor/xbar.3s.rb +0 -179
  61. data/templates/fizzy.json.example +0 -24
  62. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  63. /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")
@@ -294,9 +327,9 @@ def register_project(options)
294
327
  exit 1
295
328
  end
296
329
 
297
- print "Fizzy tags (comma-separated) [#{project_key}]: "
330
+ print "Tags (comma-separated) [#{project_key}]: "
298
331
  tags_input = $stdin.gets.chomp
299
- fizzy_tags = tags_input.empty? ? [project_key] : tags_input.split(",").map(&:strip)
332
+ project_tags = tags_input.empty? ? [project_key] : tags_input.split(",").map(&:strip)
300
333
 
301
334
  print "GitHub repo [#{info["github_repo"]}]: "
302
335
  github_repo = $stdin.gets.chomp
@@ -310,15 +343,24 @@ 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 = {
320
362
  "repo_path" => info["repo_path"],
321
- "fizzy_tags" => fizzy_tags,
363
+ "tags" => project_tags,
322
364
  "github_repo" => github_repo,
323
365
  "agent_name" => agent_name,
324
366
  "agent_model" => agent_model,
@@ -331,7 +373,7 @@ def register_project(options)
331
373
  puts
332
374
  puts "✓ Project '#{project_key}' registered successfully!"
333
375
  puts " Repo: #{info["repo_path"]}"
334
- puts " Tags: #{fizzy_tags.join(", ")}"
376
+ puts " Tags: #{project_tags.join(", ")}"
335
377
  puts " GitHub: #{github_repo}" if github_repo
336
378
  end
337
379
 
@@ -365,7 +407,7 @@ def list_projects
365
407
  default_marker = config["default"] ? " (default)" : ""
366
408
  puts " #{key}#{default_marker}"
367
409
  puts " Path: #{config["repo_path"]}"
368
- puts " Tags: #{config["fizzy_tags"].join(", ")}"
410
+ puts " Tags: #{config["tags"] || config["project_tags"].join(", ")}"
369
411
  puts " GitHub: #{config["github_repo"]}" if config["github_repo"]
370
412
  puts " Agent: #{config["agent_cli"]} (model: #{config["agent_model"]})"
371
413
  puts
@@ -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
+
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[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]"
@@ -853,7 +999,7 @@ when "discord"
853
999
  else
854
1000
  puts "Agents with Discord bots:"
855
1001
  agents_with_tokens.each do |key, entry|
856
- display = entry["fizzy_name"] || key.capitalize
1002
+ display = entry["display_name"] || key.capitalize
857
1003
  token = entry.dig("env", "DISCORD_BOT_TOKEN") || entry["discord_bot_token"]
858
1004
  token_preview = "#{token[0..10]}..."
859
1005
  puts " #{display} (#{key}): #{token_preview}"
@@ -909,31 +1055,31 @@ when "discord"
909
1055
  HELP
910
1056
  end
911
1057
 
912
- when "card-map"
913
- card_map_file = File.join(BRAINIAC_DIR, "card_map.json")
1058
+ when "work-items", "card-map"
1059
+ work_items_file = File.join(BRAINIAC_DIR, "work_items.json")
914
1060
  cm_cmd = ARGV.shift
915
1061
  case cm_cmd
916
1062
  when "clean"
917
- unless File.exist?(card_map_file)
918
- puts "No card_map.json found."
1063
+ unless File.exist?(work_items_file)
1064
+ puts "No work items found."
919
1065
  exit 0
920
1066
  end
921
- map = JSON.parse(File.read(card_map_file))
1067
+ map = JSON.parse(File.read(work_items_file))
922
1068
  before = map.size
923
1069
  # Remove entries with no valid worktree on disk
924
1070
  map.reject! { |_id, info| info["worktree"].nil? || !File.directory?(info["worktree"].to_s) }
925
1071
  after = map.size
926
1072
  removed = before - after
927
- File.write(card_map_file, JSON.pretty_generate(map))
928
- puts "Cleaned card map: removed #{removed} stale entries (#{before} → #{after})"
1073
+ File.write(work_items_file, JSON.pretty_generate(map))
1074
+ puts "Cleaned work items: removed #{removed} stale entries (#{before} → #{after})"
929
1075
  when "list", "ls", nil
930
- unless File.exist?(card_map_file)
931
- puts "No card_map.json found."
1076
+ unless File.exist?(work_items_file)
1077
+ puts "No work items found."
932
1078
  exit 0
933
1079
  end
934
- map = JSON.parse(File.read(card_map_file))
1080
+ map = JSON.parse(File.read(work_items_file))
935
1081
  if map.empty?
936
- puts "Card map is empty."
1082
+ puts "No active work items."
937
1083
  else
938
1084
  map.each_value do |info|
939
1085
  num = info["number"] ? "##{info["number"]}" : "(no number)"
@@ -943,7 +1089,7 @@ when "card-map"
943
1089
  end
944
1090
  end
945
1091
  else
946
- puts "Usage: brainiac card-map [clean|list]"
1092
+ puts "Usage: brainiac work-items [clean|list]"
947
1093
  puts " clean Remove stale entries (missing worktrees)"
948
1094
  puts " list Show all card map entries"
949
1095
  end
@@ -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,11 +1562,15 @@ 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
1408
1572
  agents = registry.select { |_, e| e.is_a?(Hash) }.map do |key, entry|
1409
- display = entry["fizzy_name"] || key.capitalize
1573
+ display = entry["display_name"] || key.capitalize
1410
1574
  details = []
1411
1575
  if entry["role"]
1412
1576
  roles = Array(entry["role"])
@@ -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, "display_name")
1618
+ registry[agent_key]["display_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
@@ -1520,7 +1725,7 @@ when "agent"
1520
1725
  persona_text = input.empty? ? nil : input
1521
1726
  end
1522
1727
 
1523
- entry = { "fizzy_name" => name }
1728
+ entry = { "display_name" => name }
1524
1729
  entry["local"] = true if local
1525
1730
  entry["cli_provider"] = cli_provider if cli_provider
1526
1731
  entry["role"] = roles.size == 1 ? roles.first : roles if roles.any?
@@ -1571,7 +1776,7 @@ when "agent"
1571
1776
  exit 1
1572
1777
  end
1573
1778
 
1574
- display = registry[agent_key]["fizzy_name"] || name
1779
+ display = registry[agent_key]["display_name"] || name
1575
1780
  registry.delete(agent_key)
1576
1781
  File.write(agent_registry_file, JSON.pretty_generate(registry))
1577
1782
  puts "✓ Removed agent '#{display}' (#{agent_key}) from registry"
@@ -1581,15 +1786,17 @@ 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)
1592
- <name> set <field> <value> Update a config field (local, cli, role, persona, fizzy_name)
1799
+ <name> set <field> <value> Update a config field (local, cli, role, persona, display_name)
1593
1800
  <name> env <KEY> <VALUE> Set an env var for an agent
1594
1801
  <name> env List env vars for an agent
1595
1802
  <name> env --delete <KEY> Remove an env var from an agent
@@ -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
@@ -1608,7 +1817,7 @@ when "agent"
1608
1817
  brainiac agent galen set local true
1609
1818
  brainiac agent galen set persona "Dry, sardonic"
1610
1819
  brainiac agent galen set role code-reviewer,general-engineer
1611
- brainiac agent galen env FIZZY_TOKEN fizzy_abc123
1820
+ brainiac agent galen env MY_TOKEN abc123
1612
1821
  brainiac agent galen show
1613
1822
  HELP
1614
1823
 
@@ -1633,11 +1842,11 @@ when "agent"
1633
1842
  puts JSON.pretty_generate(display)
1634
1843
 
1635
1844
  when "env"
1636
- if ARGV[0] == "--delete"
1845
+ if ["--delete", "unset", "delete", "rm"].include?(ARGV[0])
1637
1846
  ARGV.shift
1638
1847
  var_name = ARGV.shift
1639
1848
  unless var_name
1640
- puts "Usage: brainiac agent #{agent_cmd} env --delete <KEY>"
1849
+ puts "Usage: brainiac agent #{agent_cmd} env unset <KEY>"
1641
1850
  exit 1
1642
1851
  end
1643
1852
  registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
@@ -1689,7 +1898,7 @@ when "agent"
1689
1898
  puts " cli <provider> Set CLI provider"
1690
1899
  puts " role <roles> Set roles (comma-separated, or 'none' to clear)"
1691
1900
  puts " persona <text> Set/update persona style file"
1692
- puts " fizzy_name <name> Set display name for Fizzy @mentions"
1901
+ puts " display_name <name> Set display name for @mentions"
1693
1902
  exit 1
1694
1903
  end
1695
1904
 
@@ -1737,7 +1946,7 @@ when "agent"
1737
1946
  persona_dir = File.join(BRAINIAC_DIR, "brain", "persona", agent_key)
1738
1947
  FileUtils.mkdir_p(persona_dir)
1739
1948
  persona_file = File.join(persona_dir, "style.md")
1740
- display = registry[agent_key]["fizzy_name"] || agent_cmd.capitalize
1949
+ display = registry[agent_key]["display_name"] || agent_cmd.capitalize
1741
1950
  File.write(persona_file, <<~MD)
1742
1951
  ---
1743
1952
  name: #{agent_key}-style
@@ -1748,17 +1957,17 @@ when "agent"
1748
1957
  MD
1749
1958
  puts "✓ Wrote persona to #{persona_file}"
1750
1959
 
1751
- when "fizzy_name"
1960
+ when "display_name"
1752
1961
  unless value
1753
- puts "Usage: brainiac agent #{agent_cmd} set fizzy_name <name>"
1962
+ puts "Usage: brainiac agent #{agent_cmd} set display_name <name>"
1754
1963
  exit 1
1755
1964
  end
1756
- registry[agent_key]["fizzy_name"] = value
1965
+ registry[agent_key]["display_name"] = value
1757
1966
  File.write(agent_registry_file, JSON.pretty_generate(registry))
1758
- puts "✓ Set fizzy_name=#{value} for #{agent_cmd}"
1967
+ puts "✓ Set display_name=#{value} for #{agent_cmd}"
1759
1968
 
1760
1969
  else
1761
- puts "Unknown field '#{field}'. Available: local, cli, role, persona, fizzy_name"
1970
+ puts "Unknown field '#{field}'. Available: local, cli, role, persona, display_name"
1762
1971
  exit 1
1763
1972
  end
1764
1973
 
@@ -1793,7 +2002,7 @@ when "role"
1793
2002
  roles = entry["role"].is_a?(Array) ? entry["role"] : [entry["role"]]
1794
2003
  roles.each do |r|
1795
2004
  agent_roles[r] ||= []
1796
- agent_roles[r] << (entry["fizzy_name"] || key.capitalize)
2005
+ agent_roles[r] << (entry["display_name"] || key.capitalize)
1797
2006
  end
1798
2007
  end
1799
2008
 
@@ -1936,6 +2145,217 @@ 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> [options]"
2152
+ puts ""
2153
+ puts "Installs a Brainiac plugin gem (brainiac-<name>) and registers it."
2154
+ puts ""
2155
+ puts "Options:"
2156
+ puts " --path <dir> Use a local gem directory (for development/private gems)"
2157
+ puts " --version <ver> Install a specific version from RubyGems"
2158
+ puts ""
2159
+ puts "Examples:"
2160
+ puts " brainiac install whatsapp # from RubyGems"
2161
+ puts " brainiac install fizzy --path ~/Code/brainiac-fizzy # local dev"
2162
+ puts " brainiac install slack --version 0.2.0 # specific version"
2163
+ puts ""
2164
+ puts "After installing, restart the server: brainiac restart"
2165
+ exit 1
2166
+ end
2167
+
2168
+ # Strip "brainiac-" prefix if user includes it
2169
+ name = name.sub(/^brainiac-/, "")
2170
+ version = nil
2171
+ local_path = nil
2172
+
2173
+ # Parse flags
2174
+ while ARGV.any?
2175
+ case ARGV[0]
2176
+ when "--version", "-v"
2177
+ ARGV.shift
2178
+ version = ARGV.shift
2179
+ when "--path", "-p"
2180
+ ARGV.shift
2181
+ local_path = File.expand_path(ARGV.shift || "")
2182
+ else
2183
+ break
2184
+ end
2185
+ end
2186
+
2187
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2188
+ plugins_config = if File.exist?(plugins_file)
2189
+ JSON.parse(File.read(plugins_file))
2190
+ else
2191
+ { "plugins" => [] }
2192
+ end
2193
+
2194
+ existing = (plugins_config["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2195
+ if existing
2196
+ puts "Plugin '#{name}' is already installed."
2197
+ puts " To reinstall: brainiac uninstall #{name} && brainiac install #{name}"
2198
+ exit 0
2199
+ end
2200
+
2201
+ gem_name = "brainiac-#{name}"
2202
+
2203
+ if local_path
2204
+ # Local path mode — validate the directory and register without gem install
2205
+ unless Dir.exist?(local_path)
2206
+ puts "Error: Path does not exist: #{local_path}"
2207
+ exit 1
2208
+ end
2209
+
2210
+ entry_file = File.join(local_path, "lib", "brainiac-#{name}.rb")
2211
+ unless File.exist?(entry_file)
2212
+ puts "Error: Expected entry file not found: #{entry_file}"
2213
+ puts " Plugin gems must have lib/brainiac-<name>.rb"
2214
+ exit 1
2215
+ end
2216
+
2217
+ puts "Registering #{gem_name} from local path: #{local_path}"
2218
+
2219
+ plugins_config["plugins"] ||= []
2220
+ entry = { "name" => name, "gem" => gem_name, "path" => local_path, "installed_at" => Time.now.iso8601 }
2221
+ plugins_config["plugins"] << entry
2222
+
2223
+ ensure_brainiac_dir
2224
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
2225
+
2226
+ update_handler_config(name, true)
2227
+
2228
+ puts ""
2229
+ puts "✓ Registered plugin '#{name}' from #{local_path}"
2230
+ puts " Restart the server to activate: brainiac restart"
2231
+ puts " Note: Source changes take effect on next server restart."
2232
+ else
2233
+ # RubyGems mode
2234
+ puts "Installing #{gem_name}..."
2235
+
2236
+ install_cmd = "gem install #{gem_name}"
2237
+ install_cmd += " --version '#{version}'" if version
2238
+
2239
+ system(install_cmd)
2240
+ unless $CHILD_STATUS.success?
2241
+ puts ""
2242
+ puts "Failed to install #{gem_name}."
2243
+ puts " Check that the gem exists: https://rubygems.org/gems/#{gem_name}"
2244
+ exit 1
2245
+ end
2246
+
2247
+ # Register in plugins.json
2248
+ plugins_config["plugins"] ||= []
2249
+ entry = { "name" => name, "gem" => gem_name, "installed_at" => Time.now.iso8601 }
2250
+ entry["version"] = version if version
2251
+ plugins_config["plugins"] << entry
2252
+
2253
+ ensure_brainiac_dir
2254
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
2255
+
2256
+ # Enable the handler
2257
+ update_handler_config(name, true)
2258
+
2259
+ puts ""
2260
+ puts "✓ Installed plugin '#{name}' (#{gem_name})"
2261
+ puts " Restart the server to activate: brainiac restart"
2262
+ end
2263
+
2264
+ when "uninstall"
2265
+ name = ARGV.shift
2266
+ unless name
2267
+ puts "Usage: brainiac uninstall <plugin-name>"
2268
+ puts ""
2269
+ puts "Removes a Brainiac plugin from the configuration."
2270
+ puts "The gem itself remains installed (use 'gem uninstall brainiac-<name>' to fully remove)."
2271
+ exit 1
2272
+ end
2273
+
2274
+ name = name.sub(/^brainiac-/, "")
2275
+ gem_name = "brainiac-#{name}"
2276
+
2277
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2278
+ unless File.exist?(plugins_file)
2279
+ puts "Plugin '#{name}' is not installed (no plugins.json found)."
2280
+ exit 1
2281
+ end
2282
+
2283
+ plugins_config = JSON.parse(File.read(plugins_file))
2284
+ found = (plugins_config["plugins"] || []).any? { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2285
+
2286
+ unless found
2287
+ puts "Plugin '#{name}' is not installed."
2288
+ exit 1
2289
+ end
2290
+
2291
+ plugins_config["plugins"].reject! { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2292
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
2293
+
2294
+ # Disable the handler
2295
+ update_handler_config(name, false)
2296
+
2297
+ puts "✓ Removed plugin '#{name}' from Brainiac."
2298
+ puts " The gem #{gem_name} is still installed system-wide."
2299
+ puts " To fully remove: gem uninstall #{gem_name}"
2300
+ puts " Restart the server to apply: brainiac restart"
2301
+
2302
+ when "plugins"
2303
+ plugins_cmd = ARGV.shift
2304
+
2305
+ case plugins_cmd
2306
+ when "list", "ls", nil
2307
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2308
+ plugins_config = if File.exist?(plugins_file)
2309
+ JSON.parse(File.read(plugins_file))
2310
+ else
2311
+ { "plugins" => [] }
2312
+ end
2313
+
2314
+ plugins = plugins_config["plugins"] || []
2315
+ if plugins.empty?
2316
+ puts "No plugins installed."
2317
+ puts ""
2318
+ puts "Install plugins with: brainiac install <name>"
2319
+ puts " Example: brainiac install whatsapp"
2320
+ puts " Example: brainiac install fizzy --path ~/Code/brainiac-fizzy"
2321
+ else
2322
+ puts "Installed plugins:"
2323
+ plugins.each do |p|
2324
+ entry = p.is_a?(Hash) ? p : { "name" => p.to_s }
2325
+ name = entry["name"]
2326
+ gem_name = entry["gem"] || "brainiac-#{name}"
2327
+ local_path = entry["path"]
2328
+ installed_at = entry["installed_at"] ? " (#{entry["installed_at"][0..9]})" : ""
2329
+
2330
+ if local_path
2331
+ # Local plugin — check path exists
2332
+ if Dir.exist?(local_path)
2333
+ puts " ✓ #{name} → #{local_path}#{installed_at}"
2334
+ else
2335
+ puts " ✗ #{name} → #{local_path} (path missing)#{installed_at}"
2336
+ end
2337
+ else
2338
+ # Gem plugin — check gem is loadable
2339
+ version = entry["version"] || "latest"
2340
+ loadable = begin
2341
+ spec = Gem::Specification.find_by_name(gem_name)
2342
+ "✓ #{name} (#{gem_name} #{spec.version})"
2343
+ rescue Gem::MissingSpecError
2344
+ "✗ #{name} (#{gem_name} — gem not found)"
2345
+ end
2346
+ puts " #{loadable}#{installed_at}"
2347
+ end
2348
+ end
2349
+ end
2350
+ else
2351
+ puts "Usage: brainiac plugins [list]"
2352
+ puts ""
2353
+ puts "Managing plugins:"
2354
+ puts " brainiac install <name> Install a plugin"
2355
+ puts " brainiac uninstall <name> Remove a plugin"
2356
+ puts " brainiac plugins List installed plugins"
2357
+ end
2358
+
1939
2359
  when "version", "--version", "-v"
1940
2360
  puts "brainiac #{BRAINIAC_VERSION}"
1941
2361
 
@@ -1956,6 +2376,9 @@ when "help", "--help", "-h", nil
1956
2376
  brainiac projects list List all registered projects (alias)
1957
2377
  brainiac projects default <key> Set the default project
1958
2378
  brainiac show <key> Show project configuration
2379
+ brainiac install <plugin> Install a Brainiac plugin (gem-based handler)
2380
+ brainiac uninstall <plugin> Remove an installed plugin
2381
+ brainiac plugins List installed plugins
1959
2382
  brainiac brain <command> Manage agent long-term memory (brain)
1960
2383
  brainiac discord <command> Manage the Discord bot
1961
2384
  brainiac cron <command> Manage scheduled agent tasks
@@ -1970,6 +2393,11 @@ when "help", "--help", "-h", nil
1970
2393
  Server Options:
1971
2394
  -d, --daemon Run server in background (detached)
1972
2395
 
2396
+ Plugin Commands:
2397
+ install <name> Install a plugin (gem install brainiac-<name>)
2398
+ uninstall <name> Remove a plugin
2399
+ plugins List installed plugins
2400
+
1973
2401
  Card Map Commands:
1974
2402
  card-map list Show all card map entries
1975
2403
  card-map clean Remove stale entries (missing worktrees)