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/bin/brainiac CHANGED
@@ -912,46 +912,85 @@ when "setup"
912
912
  end
913
913
  end
914
914
 
915
- # Handler configuration
916
- brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
915
+ # Agent setup — agents.json is the authority
916
+ agents_file = File.join(BRAINIAC_DIR, "agents.json")
917
+ existing_agents = if File.exist?(agents_file)
918
+ registry = JSON.parse(File.read(agents_file))
919
+ registry.keys
920
+ else
921
+ []
922
+ end
923
+
917
924
  puts ""
918
- if File.exist?(brainiac_config_path)
919
- puts " Handler config already exists at ~/.brainiac/brainiac.json"
925
+ if existing_agents.empty?
926
+ puts "No agents found in ~/.brainiac/agents.json"
927
+ print "Create your first agent? Name: "
928
+ agent_name = $stdin.gets&.strip
929
+ if agent_name && !agent_name.empty?
930
+ agent_key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
931
+ registry = { agent_key => { "display_name" => agent_name, "local" => true, "env" => {} } }
932
+ File.write(agents_file, JSON.pretty_generate(registry))
933
+ puts "✓ Created #{agents_file} with agent '#{agent_name}'"
934
+ existing_agents = [agent_key]
935
+ else
936
+ agent_name = nil
937
+ puts " Skipped — add agents to ~/.brainiac/agents.json later"
938
+ end
920
939
  else
921
- puts "Which integrations do you want to enable?"
922
- puts "(You can change this later in ~/.brainiac/brainiac.json)"
923
- puts ""
924
-
925
- available_handlers = {
926
- "github" => "GitHub (PR reviews, CI failure handling, deploy tracking)",
927
- "zoho" => "Zoho Mail (email notifications and triage)"
928
- }
940
+ puts "Found #{existing_agents.size} agent(s): #{existing_agents.join(", ")}"
941
+ end
929
942
 
930
- enabled_handlers = {}
931
- available_handlers.each do |key, description|
932
- print " Enable #{description}? [Y/n] "
933
- answer = $stdin.gets&.strip&.downcase
934
- enabled_handlers[key] = answer != "n"
943
+ # Default agent selection
944
+ brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
945
+ puts ""
946
+ if File.exist?(brainiac_config_path)
947
+ current = begin
948
+ JSON.parse(File.read(brainiac_config_path)).fetch("default_agent", nil)
949
+ rescue StandardError
950
+ nil
935
951
  end
936
-
937
- config = { "handlers" => enabled_handlers }
952
+ puts "✓ Config already exists at ~/.brainiac/brainiac.json (default_agent: #{current})"
953
+ else
954
+ default_agent = if existing_agents.size == 1
955
+ name = existing_agents.first
956
+ # Look up display name if available
957
+ display = begin
958
+ JSON.parse(File.read(agents_file)).dig(name, "display_name") || name.capitalize
959
+ rescue StandardError
960
+ name.capitalize
961
+ end
962
+ print "Set #{display} as default agent? [Y/n] "
963
+ answer = $stdin.gets&.strip&.downcase
964
+ answer == "n" ? nil : display
965
+ elsif existing_agents.size > 1
966
+ puts "Choose default agent:"
967
+ display_names = existing_agents.map do |key|
968
+ JSON.parse(File.read(agents_file)).dig(key, "display_name") || key.capitalize
969
+ rescue StandardError
970
+ key.capitalize
971
+ end
972
+ display_names.each_with_index { |a, i| puts " #{i + 1}) #{a}" }
973
+ print "Enter number [1]: "
974
+ choice = $stdin.gets&.strip
975
+ idx = choice.to_i - 1
976
+ idx = 0 if idx.negative? || idx >= display_names.size
977
+ display_names[idx]
978
+ else
979
+ agent_name
980
+ end
981
+
982
+ default_agent ||= existing_agents.first&.capitalize
983
+ config = { "default_agent" => default_agent }
938
984
  File.write(brainiac_config_path, JSON.pretty_generate(config))
939
- puts ""
940
- puts "✓ Wrote handler config to ~/.brainiac/brainiac.json"
941
-
942
- enabled_list = enabled_handlers.select { |_, v| v }.keys
943
- disabled_list = enabled_handlers.reject { |_, v| v }.keys
944
- puts " Enabled: #{enabled_list.join(", ")}" if enabled_list.any?
945
- puts " Disabled: #{disabled_list.join(", ")}" if disabled_list.any?
985
+ puts "✓ Wrote config to ~/.brainiac/brainiac.json (default_agent: #{default_agent})"
946
986
  end
947
987
 
948
988
  puts ""
949
989
  puts "Next steps:"
950
- puts " 1. Edit config files in ~/.brainiac/ with your secrets and IDs"
951
- puts " 2. Create agent configs in ~/.kiro/agents/"
952
- puts " 3. Register projects: cd ~/Code/myproject && brainiac register"
953
- puts " 4. Initialize brain: brainiac brain init"
954
- puts " 5. Start server: brainiac server"
990
+ puts " 1. Register projects: cd ~/Code/myproject && brainiac register"
991
+ puts " 2. Initialize brain: brainiac brain init"
992
+ puts " 3. Install plugins: brainiac install discord / fizzy / github / zoho"
993
+ puts " 4. Start server: brainiac server"
955
994
 
956
995
  # Shell completion setup
957
996
  puts ""
@@ -1054,64 +1093,6 @@ when "show"
1054
1093
  end
1055
1094
  show_project(ARGV[0])
1056
1095
 
1057
- when "handler", "handlers"
1058
- handler_cmd = ARGV.shift
1059
- case handler_cmd
1060
- when "list", "ls", nil
1061
- brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
1062
- config = if File.exist?(brainiac_config_path)
1063
- JSON.parse(File.read(brainiac_config_path))
1064
- else
1065
- {}
1066
- end
1067
- handlers = config["handlers"] || {}
1068
-
1069
- # Built-in handlers
1070
- builtins = %w[github zoho]
1071
- puts "Built-in handlers:"
1072
- builtins.each do |name|
1073
- enabled = handlers.fetch(name, true)
1074
- status = enabled ? "✓ enabled" : "✗ disabled"
1075
- puts " #{name.ljust(10)} #{status}"
1076
- end
1077
-
1078
- # Custom handlers
1079
- custom_dir = File.join(BRAINIAC_DIR, "handlers")
1080
- if Dir.exist?(custom_dir)
1081
- custom_files = Dir.glob(File.join(custom_dir, "*.rb"))
1082
- if custom_files.any?
1083
- puts ""
1084
- puts "Custom handlers (#{custom_dir}):"
1085
- custom_files.each do |f|
1086
- name = File.basename(f, ".rb")
1087
- enabled = handlers.fetch(name, true)
1088
- status = enabled ? "✓ enabled" : "✗ disabled"
1089
- puts " #{name.ljust(10)} #{status}"
1090
- end
1091
- end
1092
- end
1093
-
1094
- when "enable"
1095
- name = ARGV.shift
1096
- unless name
1097
- puts "Usage: brainiac handler enable <name>"
1098
- exit 1
1099
- end
1100
- update_handler_config(name, true)
1101
-
1102
- when "disable"
1103
- name = ARGV.shift
1104
- unless name
1105
- puts "Usage: brainiac handler disable <name>"
1106
- exit 1
1107
- end
1108
- update_handler_config(name, false)
1109
-
1110
- else
1111
- puts "Usage: brainiac handler <list|enable|disable> [name]"
1112
- exit 1
1113
- end
1114
-
1115
1096
  when "config"
1116
1097
  OptionParser.new do |opts|
1117
1098
  opts.banner = "Usage: brainiac config [options]"
@@ -1322,8 +1303,8 @@ when "cron"
1322
1303
  opts.on("-p", "--project KEY", "Project key") { |v| project = v }
1323
1304
  opts.on("-m", "--model MODEL", "Model to use (opus, sonnet, haiku, auto)") { |v| model = v }
1324
1305
  opts.on("-e", "--effort LEVEL", "Effort level (low, medium, high, xhigh, max)") { |v| effort = v }
1325
- opts.on("-d", "--notify TARGET", "Notification target (channel ID, card number, etc.)") { |v| notify_target = v }
1326
- opts.on("-c", "--channel CHANNEL", "Notification channel (discord, fizzy — default: discord)") { |v| notify_channel = v }
1306
+ opts.on("-d", "--notify TARGET", "Notification target (channel ID, webhook URL, etc.)") { |v| notify_target = v }
1307
+ opts.on("-c", "--channel CHANNEL", "Notification channel name (provided by plugins)") { |v| notify_channel = v }
1327
1308
  opts.on("-t", "--title TITLE", "Forum post title (for forum channels)") { |v| forum_title = v }
1328
1309
  opts.on("-r", "--repeat COUNT", Integer, "Number of times to repeat (for recurring schedules)") { |v| repeat_count = v }
1329
1310
  opts.on("--script PATH", "Run script directly (no agent, output posted via notify)") { |v| script = v }
@@ -1366,7 +1347,6 @@ when "cron"
1366
1347
  payload[:script] = script if script
1367
1348
  payload[:model] = model if model
1368
1349
  payload[:effort] = effort if effort
1369
- payload[:discord_channel_id] = notify_target if notify_target && (!notify_channel || notify_channel == "discord")
1370
1350
  payload[:notify_channel] = notify_channel if notify_channel
1371
1351
  payload[:notify_target] = notify_target if notify_target
1372
1352
  payload[:forum_title] = forum_title if forum_title
@@ -1383,7 +1363,7 @@ when "cron"
1383
1363
  puts " Project: #{project}"
1384
1364
  puts " Model: #{model || "default"}" unless script
1385
1365
  puts " Effort: #{effort || "default"}" unless script
1386
- puts " Notify: #{notify_target ? "#{notify_channel || "discord"} → #{notify_target}" : "none"}"
1366
+ puts " Notify: #{notify_target ? "#{notify_channel || "default"} → #{notify_target}" : "none"}"
1387
1367
  puts " Forum title: #{forum_title || "auto"}" if notify_target && forum_title
1388
1368
  puts " Repeat: #{repeat_count ? "#{repeat_count} times" : "unlimited"}"
1389
1369
  puts script ? " Script: #{script}" : " Prompt: #{prompt}"
@@ -1428,9 +1408,9 @@ when "cron"
1428
1408
  puts " #{status} #{id}"
1429
1409
  puts " Schedule: #{schedule_display}"
1430
1410
  puts " Agent: #{job["script"] ? "none (script mode)" : job["agent"]} | Project: #{job["project"]}"
1431
- notify_ch = job["notify_channel"] || (job["discord_channel_id"] ? "discord" : nil)
1432
- notify_tgt = job["notify_target"] || job["discord_channel_id"]
1433
- puts " Notify: #{notify_ch} → #{notify_tgt}" if notify_tgt
1411
+ notify_ch = job["notify_channel"]
1412
+ notify_tgt = job["notify_target"]
1413
+ puts " Notify: #{notify_ch || "default"} → #{notify_tgt}" if notify_tgt
1434
1414
  puts job["script"] ? " Script: #{job["script"]}" : " Prompt: #{job["prompt"]}"
1435
1415
  puts " Last run: #{last_run}"
1436
1416
  puts
@@ -1527,14 +1507,14 @@ when "cron"
1527
1507
  opts.banner = "Usage: brainiac cron update <job-id> [options]"
1528
1508
  opts.on("-s", "--schedule EXPR", "New cron expression") { |v| schedule = v }
1529
1509
  opts.on("-d", "--notify TARGET", "New notification target") { |v| notify_target = v }
1530
- opts.on("-c", "--channel CHANNEL", "New notification channel (discord, fizzy)") { |v| notify_channel = v }
1510
+ opts.on("-c", "--channel CHANNEL", "New notification channel") { |v| notify_channel = v }
1531
1511
  opts.on("-t", "--title TITLE", "New forum post title") { |v| forum_title = v }
1532
1512
  end.parse!
1533
1513
 
1534
1514
  unless id && (schedule || notify_target || notify_channel || forum_title)
1535
1515
  puts "Error: Missing required arguments"
1536
1516
  puts "Usage: brainiac cron update <job-id> -s \"42 13 * * 1-5\""
1537
- puts " or: brainiac cron update <job-id> -d \"channel-id\" -c discord"
1517
+ puts " or: brainiac cron update <job-id> -d \"channel-id\" -c channel-name"
1538
1518
  puts " or: brainiac cron update <job-id> -t \"Daily Update\""
1539
1519
  exit 1
1540
1520
  end
@@ -1543,7 +1523,6 @@ when "cron"
1543
1523
  req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
1544
1524
  payload = { id: id }
1545
1525
  payload[:schedule] = schedule if schedule
1546
- payload[:discord_channel_id] = notify_target if notify_target
1547
1526
  payload[:notify_channel] = notify_channel if notify_channel
1548
1527
  payload[:notify_target] = notify_target if notify_target
1549
1528
  payload[:forum_title] = forum_title if forum_title
@@ -1555,7 +1534,7 @@ when "cron"
1555
1534
  if data["success"]
1556
1535
  puts "✓ Updated cron job: #{id}"
1557
1536
  puts " New schedule: #{schedule}" if schedule
1558
- puts " New notify: #{notify_channel || "discord"} → #{notify_target}" if notify_target
1537
+ puts " New notify: #{notify_channel || "default"} → #{notify_target}" if notify_target
1559
1538
  puts " New forum title: #{forum_title}" if forum_title
1560
1539
  else
1561
1540
  puts "Error: #{data["error"]}"
@@ -1604,7 +1583,7 @@ when "cron"
1604
1583
  Examples:
1605
1584
  # Recurring
1606
1585
  brainiac cron add -s "0 9 * * *" -p marketplace "Summarize yesterday's work"
1607
- brainiac cron add -s "@daily" -p brainiac -a GLaDOS "Run security audit"
1586
+ brainiac cron add -s "@daily" -p brainiac -a Robin "Run security audit"
1608
1587
  #{" "}
1609
1588
  # One-time
1610
1589
  brainiac cron add -s "tomorrow at 9am" -p marketplace "Reminder about priorities"
@@ -1787,10 +1766,10 @@ when "agent"
1787
1766
  puts "If no options are given and stdin is a terminal, interactive mode is used."
1788
1767
  puts ""
1789
1768
  puts "Examples:"
1769
+ puts " brainiac agent create MyAgent # interactive"
1790
1770
  puts " brainiac agent create SecurityBot --local --role code-reviewer"
1791
1771
  puts " brainiac agent create Jane --role general-engineer --cli grok"
1792
- puts " brainiac agent create Avon --persona \"Dry wit, sardonic, brilliant\""
1793
- puts " brainiac agent create MyAgent # interactive"
1772
+ puts " brainiac agent create Merlin --persona \"Wise, enigmatic, helpful\""
1794
1773
  exit 1
1795
1774
  end
1796
1775
 
@@ -1941,16 +1920,16 @@ when "agent"
1941
1920
 
1942
1921
  Examples:
1943
1922
  brainiac agent list
1944
- brainiac agent default Galen
1923
+ brainiac agent default Sherlock
1945
1924
  brainiac agent create SecurityBot --local --role code-reviewer
1946
1925
  brainiac agent create Jane --cli grok
1947
1926
  brainiac agent create MyAgent # interactive mode
1948
1927
  brainiac agent remove SecurityBot
1949
- brainiac agent galen set local true
1950
- brainiac agent galen set persona "Dry, sardonic"
1951
- brainiac agent galen set role code-reviewer,general-engineer
1952
- brainiac agent galen env MY_TOKEN abc123
1953
- brainiac agent galen show
1928
+ brainiac agent sherlock set local true
1929
+ brainiac agent sherlock set persona "Dry, sardonic"
1930
+ brainiac agent sherlock set role code-reviewer,general-engineer
1931
+ brainiac agent sherlock env MY_TOKEN abc123
1932
+ brainiac agent sherlock show
1954
1933
  HELP
1955
1934
 
1956
1935
  else
@@ -2277,6 +2256,96 @@ when "completions"
2277
2256
  puts File.read(completion_file)
2278
2257
  end
2279
2258
 
2259
+ when "update", "upgrade"
2260
+ name = ARGV.shift
2261
+
2262
+ if name
2263
+ # Update a specific plugin: brainiac update <plugin_name>
2264
+ name = name.sub(/^brainiac-/, "")
2265
+ gem_name = "brainiac-#{name}"
2266
+
2267
+ # Verify plugin is installed
2268
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
2269
+ plugins_config = if File.exist?(plugins_file)
2270
+ JSON.parse(File.read(plugins_file))
2271
+ else
2272
+ { "plugins" => [] }
2273
+ end
2274
+
2275
+ plugin_entry = (plugins_config["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
2276
+
2277
+ if plugin_entry.is_a?(Hash) && plugin_entry["path"]
2278
+ puts "Plugin '#{name}' is installed from a local path: #{plugin_entry["path"]}"
2279
+ puts "Local plugins are updated via git pull in their source directory."
2280
+ exit 0
2281
+ end
2282
+
2283
+ unless plugin_entry
2284
+ puts "Plugin '#{name}' is not installed."
2285
+ puts " Install it first: brainiac install #{name}"
2286
+ exit 1
2287
+ end
2288
+
2289
+ # Get current version before update
2290
+ current_version = begin
2291
+ spec = Gem::Specification.find_by_name(gem_name)
2292
+ spec.version.to_s
2293
+ rescue Gem::MissingSpecError
2294
+ nil
2295
+ end
2296
+
2297
+ puts "Updating #{gem_name}..."
2298
+ system("gem install #{gem_name}")
2299
+ unless $CHILD_STATUS.success?
2300
+ puts ""
2301
+ puts "Failed to update #{gem_name}."
2302
+ exit 1
2303
+ end
2304
+
2305
+ # Show version change
2306
+ Gem::Specification.reset
2307
+ new_version = begin
2308
+ spec = Gem::Specification.find_by_name(gem_name)
2309
+ spec.version.to_s
2310
+ rescue Gem::MissingSpecError
2311
+ "unknown"
2312
+ end
2313
+
2314
+ if current_version && current_version == new_version
2315
+ puts "✓ #{gem_name} is already at the latest version (#{new_version})"
2316
+ else
2317
+ puts "✓ Updated #{gem_name}: #{current_version || "new"} → #{new_version}"
2318
+ puts " Restart the server to activate: brainiac restart"
2319
+ end
2320
+ else
2321
+ # Update brainiac itself: brainiac update
2322
+ current_version = BRAINIAC_VERSION
2323
+
2324
+ puts "Updating brainiac gem..."
2325
+ system("gem install brainiac")
2326
+ unless $CHILD_STATUS.success?
2327
+ puts ""
2328
+ puts "Failed to update brainiac."
2329
+ exit 1
2330
+ end
2331
+
2332
+ # Show version change
2333
+ Gem::Specification.reset
2334
+ new_version = begin
2335
+ spec = Gem::Specification.find_by_name("brainiac")
2336
+ spec.version.to_s
2337
+ rescue Gem::MissingSpecError
2338
+ "unknown"
2339
+ end
2340
+
2341
+ if current_version == new_version
2342
+ puts "✓ brainiac is already at the latest version (#{new_version})"
2343
+ else
2344
+ puts "✓ Updated brainiac: #{current_version} → #{new_version}"
2345
+ puts " Restart the server to apply: brainiac restart"
2346
+ end
2347
+ end
2348
+
2280
2349
  when "install"
2281
2350
  name = ARGV.shift
2282
2351
  unless name
@@ -3004,6 +3073,8 @@ when "help", "--help", "-h", nil
3004
3073
  brainiac projects default <key> Set the default project
3005
3074
  brainiac show <key> Show project configuration
3006
3075
  brainiac plugin new <name> Generate a new plugin gem skeleton
3076
+ brainiac update Update brainiac to the latest version
3077
+ brainiac update <plugin> Update an installed plugin to the latest version
3007
3078
  brainiac install <plugin> Install a Brainiac plugin (gem-based handler)
3008
3079
  brainiac uninstall <plugin> Remove an installed plugin
3009
3080
  brainiac plugins List installed plugins
@@ -3024,6 +3095,7 @@ when "help", "--help", "-h", nil
3024
3095
  plugin new <name> Generate a new plugin gem skeleton
3025
3096
  install <name> Install a plugin (gem install brainiac-<name>)
3026
3097
  uninstall <name> Remove a plugin
3098
+ update [name] Update brainiac or a specific plugin
3027
3099
  plugins List installed plugins
3028
3100
 
3029
3101
  Card Map Commands:
data/brainiac.gemspec CHANGED
@@ -3,9 +3,10 @@ require_relative "lib/brainiac/version"
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "brainiac"
5
5
  s.version = Brainiac::VERSION
6
- s.summary = "AI agent webhook receiver and dispatcher"
7
- s.description = "Webhook receiver that listens for GitHub and Zoho Mail events, then dispatches work to AI agent CLIs. " \
8
- "Additional channels (Discord, Fizzy) available via plugins."
6
+ s.summary = "Multi-agent orchestration layer for developer workflows"
7
+ s.description = "Core orchestration engine that manages AI agent identity, long-term memory (brain), " \
8
+ "prompt construction, and dispatch. Communication channels are provided by plugins: " \
9
+ "brainiac-discord, brainiac-fizzy, brainiac-github, and more."
9
10
  s.authors = ["Andy Davis"]
10
11
  s.homepage = "https://github.com/stowzilla/brainiac"
11
12
  s.license = "MIT"
@@ -10,39 +10,19 @@ This file configures how agents appear in the status bar — waybar on Linux, xb
10
10
  {
11
11
  "agents": [
12
12
  {
13
- "name": "GLaDOS",
14
- "emoji": "🤖",
15
- "color": "blue"
16
- },
17
- {
18
- "name": "Galen",
19
- "emoji": "🛠️",
13
+ "name": "Sherlock",
14
+ "emoji": "🔍",
20
15
  "color": "green"
21
16
  },
22
17
  {
23
- "name": "Threepio",
24
- "emoji": "📝",
25
- "color": "yellow"
18
+ "name": "Robin",
19
+ "emoji": "🏹",
20
+ "color": "blue"
26
21
  },
27
22
  {
28
- "name": "Sheogorath",
29
- "emoji": "🎭",
23
+ "name": "Merlin",
24
+ "emoji": "🧙",
30
25
  "color": "purple"
31
- },
32
- {
33
- "name": "Kaylee",
34
- "emoji": "🔧",
35
- "color": "pink"
36
- },
37
- {
38
- "name": "Avon",
39
- "emoji": "🔐",
40
- "color": "red"
41
- },
42
- {
43
- "name": "Sleeper Service",
44
- "emoji": "💤",
45
- "color": "cyan"
46
26
  }
47
27
  ],
48
28
  "default_emoji": "❓",
@@ -6,12 +6,12 @@
6
6
  # environment variable can be set per-agent:
7
7
  #
8
8
  # {
9
- # "galen": {
10
- # "display_name": "Galen",
9
+ # "sherlock": {
10
+ # "display_name": "Sherlock",
11
11
  # "local": true,
12
12
  # "env": {
13
13
  # "SOME_TOKEN": "token_abc...",
14
- # "DISCORD_BOT_TOKEN": "Bot_abc..."
14
+ # "OTHER_TOKEN": "Bot_abc..."
15
15
  # }
16
16
  # }
17
17
  # }
@@ -71,7 +71,7 @@ def agent_env_var(agent_name, var_name)
71
71
  end
72
72
 
73
73
  # Get the display name for an agent (from agents.json registry).
74
- # This is the human-readable canonical name (e.g., "GLaDOS" not "glados").
74
+ # This is the human-readable canonical name (e.g., "Robin" not "robin").
75
75
  # Core owns this — it's the identity used across all channels and plugins.
76
76
  def agent_display_name(agent_name)
77
77
  return agent_name unless agent_name
@@ -173,7 +173,7 @@ def detect_mentioned_agent(text)
173
173
  all_agent_names.each do |name|
174
174
  return name if downcased.include?("@#{name.downcase}")
175
175
 
176
- # Some systems render mentions using first name only (e.g. "@Sleeper" not "@Sleeper Service").
176
+ # Some systems render mentions using first name only (e.g. "@Robin" not "@Robin Hood").
177
177
  # Fall back to matching the first word of multi-word agent names.
178
178
  first_word = name.split.first.downcase
179
179
  next if first_word == name.downcase # already checked above
@@ -46,15 +46,6 @@ AI_AGENT_NAME = ENV.fetch("AI_AGENT_NAME", nil) || BRAINIAC_CONFIG["default_agen
46
46
  MSG
47
47
  end
48
48
 
49
- # Check whether a handler is enabled. Defaults to true when brainiac.json
50
- # doesn't exist or doesn't list the handler (backwards compatible — all on).
51
- def handler_enabled?(name)
52
- handlers = BRAINIAC_CONFIG["handlers"]
53
- return true unless handlers # No config = everything enabled
54
-
55
- handlers.fetch(name.to_s, true)
56
- end
57
-
58
49
  LOG_LEVEL = ENV.fetch("LOG_LEVEL", "info").downcase
59
50
  LOG = Logger.new($stdout)
60
51
  LOG.level = case LOG_LEVEL
@@ -78,26 +69,6 @@ KNOWLEDGE_COLLECTION = "brainiac-knowledge"
78
69
 
79
70
  ROLES_DIR = File.join(BRAINIAC_DIR, "roles")
80
71
 
81
- # --- GitHub auth ---
82
-
83
- GITHUB_CONFIG_FILE = File.join(BRAINIAC_DIR, "github.json")
84
-
85
- def load_github_config
86
- return {} unless File.exist?(GITHUB_CONFIG_FILE)
87
-
88
- JSON.parse(File.read(GITHUB_CONFIG_FILE))
89
- rescue JSON::ParserError => e
90
- LOG.error "Failed to parse GitHub config: #{e.message}"
91
- {}
92
- end
93
-
94
- GITHUB_CONFIG = load_github_config
95
-
96
- def github_webhook_secret
97
- # Fallback to env var
98
- GITHUB_CONFIG["webhook_secret"] || ENV.fetch("GITHUB_WEBHOOK_SECRET", nil)
99
- end
100
-
101
72
  NOTIFICATION_COMMAND = ENV.fetch("NOTIFICATION_COMMAND", nil)
102
73
 
103
74
  # --- Projects ---
@@ -137,13 +108,6 @@ def reload_projects!(force: false)
137
108
  LOG.info "Reloaded projects configuration: #{PROJECTS.keys.join(", ")}"
138
109
  end
139
110
 
140
- def reload_github_config!(force: false)
141
- return unless file_changed?(GITHUB_CONFIG_FILE, force: force)
142
-
143
- GITHUB_CONFIG.replace(load_github_config)
144
- LOG.info "Reloaded GitHub configuration"
145
- end
146
-
147
111
  PROJECTS = load_projects_config
148
112
 
149
113
  DEFAULT_PROJECT = {
data/lib/brainiac/cron.rb CHANGED
@@ -199,17 +199,14 @@ def reload_cron_jobs!(force: false)
199
199
  end
200
200
 
201
201
  # Add a new cron job
202
- def add_cron_job(id:, schedule:, agent:, project:, prompt: nil, script: nil, enabled: true, model: nil, effort: nil, discord_channel_id: nil,
202
+ def add_cron_job(id:, schedule:, agent:, project:, prompt: nil, script: nil, enabled: true, model: nil, effort: nil,
203
203
  notify_channel: nil, notify_target: nil,
204
204
  forum_title: nil, forum_reply_to_latest: false, repeat_count: nil)
205
205
  parsed = parse_cron_expression(schedule)
206
206
  return { error: "Invalid cron expression" } unless parsed
207
207
  return { error: "Must provide either prompt or script, not both" } if prompt && script
208
208
  return { error: "Must provide either prompt or script" } unless prompt || script
209
-
210
- # Normalize: discord_channel_id is legacy shorthand for notify_channel: discord, notify_target: <id>
211
- notify_channel ||= :discord if discord_channel_id
212
- notify_target ||= discord_channel_id
209
+ return { error: "notify_target requires notify_channel (e.g. -c discord)" } if notify_target && !notify_channel
213
210
 
214
211
  job = {
215
212
  id: id,
@@ -224,7 +221,6 @@ def add_cron_job(id:, schedule:, agent:, project:, prompt: nil, script: nil, ena
224
221
  enabled: enabled,
225
222
  notify_channel: notify_channel&.to_s,
226
223
  notify_target: notify_target,
227
- discord_channel_id: discord_channel_id,
228
224
  forum_title: forum_title,
229
225
  forum_reply_to_latest: forum_reply_to_latest,
230
226
  repeat_count: repeat_count,
@@ -269,9 +265,9 @@ def toggle_cron_job(id, enabled)
269
265
  end
270
266
  end
271
267
 
272
- # Update a cron job's schedule, discord channel, and/or forum title
273
- def update_cron_job(id, schedule: nil, discord_channel_id: nil, forum_title: nil, forum_reply_to_latest: nil)
274
- return { error: "No updates provided" } if schedule.nil? && discord_channel_id.nil? && forum_title.nil? && forum_reply_to_latest.nil?
268
+ # Update a cron job's schedule, notification target, and/or forum title
269
+ def update_cron_job(id, schedule: nil, notify_target: nil, notify_channel: nil, forum_title: nil, forum_reply_to_latest: nil)
270
+ return { error: "No updates provided" } if schedule.nil? && notify_target.nil? && forum_title.nil? && forum_reply_to_latest.nil?
275
271
 
276
272
  if schedule
277
273
  parsed = parse_cron_expression(schedule)
@@ -287,7 +283,8 @@ def update_cron_job(id, schedule: nil, discord_channel_id: nil, forum_title: nil
287
283
  job[:schedule] = schedule
288
284
  job[:parsed] = parsed
289
285
  end
290
- job[:discord_channel_id] = discord_channel_id if discord_channel_id
286
+ job[:notify_target] = notify_target if notify_target
287
+ job[:notify_channel] = notify_channel if notify_channel
291
288
  job[:forum_title] = forum_title if forum_title
292
289
  job[:forum_reply_to_latest] = forum_reply_to_latest unless forum_reply_to_latest.nil?
293
290
  jobs[id.to_sym] = job
@@ -315,7 +312,7 @@ def execute_script_job(job, project)
315
312
  log_file = File.join(project["repo_path"], "tmp/cron-script-#{job[:id]}-#{timestamp}.log")
316
313
  FileUtils.mkdir_p(File.dirname(log_file))
317
314
 
318
- draft_file = prepare_script_notification_draft(job, timestamp) if job[:notify_target] || job[:discord_channel_id]
315
+ draft_file = prepare_script_notification_draft(job, timestamp) if job[:notify_target]
319
316
 
320
317
  LOG.info "[Cron] Running script #{script_path} for job #{job[:id]}, tail -f #{log_file}"
321
318
 
@@ -343,8 +340,8 @@ def prepare_script_notification_draft(job, timestamp)
343
340
 
344
341
  script_agent_key = job[:agent]&.downcase&.gsub(/[^a-z0-9-]/, "-")
345
342
  meta = {
346
- notify_channel: (job[:notify_channel] || "discord").to_s,
347
- notify_target: job[:notify_target] || job[:discord_channel_id],
343
+ notify_channel: job[:notify_channel]&.to_s,
344
+ notify_target: job[:notify_target],
348
345
  agent_key: script_agent_key,
349
346
  agent_name: job[:agent] || "Script",
350
347
  cron_job_id: job[:id],
@@ -361,9 +358,10 @@ def deliver_script_output(job, log_file, draft_file)
361
358
  return unless File.exist?(log_file)
362
359
 
363
360
  output = File.read(log_file).strip
364
- has_notify = job[:notify_target] || job[:discord_channel_id]
361
+ has_notify = job[:notify_target]
365
362
 
366
363
  if has_notify && draft_file && !output.empty?
364
+ LOG.warn "[Cron] Job #{job[:id]} has notify_target but no notify_channel — notification may not be delivered" unless job[:notify_channel]
367
365
  File.write(draft_file, output)
368
366
  # Deliver via notification system
369
367
  notify_cron_output(job, output, agent_name: job[:agent])
@@ -380,7 +378,7 @@ def build_cron_prompt(job, project)
380
378
  prompt = job[:prompt]
381
379
  agent_name = job[:agent]
382
380
  timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
383
- has_notify = job[:notify_target] || job[:discord_channel_id]
381
+ has_notify = job[:notify_target]
384
382
 
385
383
  if has_notify
386
384
  notify_dir = File.join(BRAINIAC_DIR, "tmp", "notify", "draft")
@@ -389,8 +387,8 @@ def build_cron_prompt(job, project)
389
387
  meta_file = "#{draft_file}.meta.json"
390
388
 
391
389
  agent_key = agent_name.downcase.gsub(/[^a-z0-9-]/, "-")
392
- notify_channel = (job[:notify_channel] || "discord").to_s
393
- notify_target = job[:notify_target] || job[:discord_channel_id]
390
+ notify_channel = job[:notify_channel]&.to_s
391
+ notify_target = job[:notify_target]
394
392
 
395
393
  meta = {
396
394
  notify_channel: notify_channel,