brainiac 0.0.11 → 0.0.13
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +181 -152
- data/bin/brainiac +89 -110
- data/brainiac.gemspec +4 -3
- data/docs/waybar-config.md +7 -27
- data/lib/brainiac/agents.rb +5 -5
- data/lib/brainiac/config.rb +1 -36
- data/lib/brainiac/cron.rb +15 -17
- data/lib/brainiac/handlers/shared/inline_tags.rb +43 -13
- data/lib/brainiac/helpers.rb +216 -32
- data/lib/brainiac/hooks.rb +1 -1
- data/lib/brainiac/notifications.rb +2 -2
- data/lib/brainiac/prompts.rb +11 -133
- data/lib/brainiac/restart.rb +0 -3
- data/lib/brainiac/routes/api.rb +6 -32
- data/lib/brainiac/sessions.rb +1 -2
- data/lib/brainiac/version.rb +1 -1
- data/monitor/shared.rb +1 -1
- data/receiver.rb +2 -227
- data/templates/agents.json.example +2 -2
- data/templates/brainiac.json.example +1 -6
- data/templates/hooks/pre-commit +14 -0
- data/templates/testflight.json.example +1 -1
- data/templates/users.json.example +20 -82
- metadata +6 -9
- data/lib/brainiac/handlers/github.rb +0 -390
- data/lib/brainiac/handlers/zoho.rb +0 -429
- data/lib/brainiac/zoho_mail_api.rb +0 -110
- data/templates/github.json.example +0 -4
- data/templates/zoho.json.example +0 -27
data/bin/brainiac
CHANGED
|
@@ -912,46 +912,85 @@ when "setup"
|
|
|
912
912
|
end
|
|
913
913
|
end
|
|
914
914
|
|
|
915
|
-
#
|
|
916
|
-
|
|
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
|
|
919
|
-
puts "
|
|
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 "
|
|
922
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
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.
|
|
951
|
-
puts " 2.
|
|
952
|
-
puts " 3.
|
|
953
|
-
puts " 4.
|
|
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,
|
|
1326
|
-
opts.on("-c", "--channel CHANNEL", "Notification channel (
|
|
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 || "
|
|
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"]
|
|
1432
|
-
notify_tgt = job["notify_target"]
|
|
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
|
|
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
|
|
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 || "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1950
|
-
brainiac agent
|
|
1951
|
-
brainiac agent
|
|
1952
|
-
brainiac agent
|
|
1953
|
-
brainiac agent
|
|
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
|
|
@@ -2295,7 +2274,7 @@ when "update", "upgrade"
|
|
|
2295
2274
|
|
|
2296
2275
|
plugin_entry = (plugins_config["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == name }
|
|
2297
2276
|
|
|
2298
|
-
if plugin_entry
|
|
2277
|
+
if plugin_entry.is_a?(Hash) && plugin_entry["path"]
|
|
2299
2278
|
puts "Plugin '#{name}' is installed from a local path: #{plugin_entry["path"]}"
|
|
2300
2279
|
puts "Local plugins are updated via git pull in their source directory."
|
|
2301
2280
|
exit 0
|
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 = "
|
|
7
|
-
s.description = "
|
|
8
|
-
"
|
|
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"
|
data/docs/waybar-config.md
CHANGED
|
@@ -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": "
|
|
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": "
|
|
24
|
-
"emoji": "
|
|
25
|
-
"color": "
|
|
18
|
+
"name": "Robin",
|
|
19
|
+
"emoji": "🏹",
|
|
20
|
+
"color": "blue"
|
|
26
21
|
},
|
|
27
22
|
{
|
|
28
|
-
"name": "
|
|
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": "❓",
|
data/lib/brainiac/agents.rb
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
# environment variable can be set per-agent:
|
|
7
7
|
#
|
|
8
8
|
# {
|
|
9
|
-
# "
|
|
10
|
-
# "display_name": "
|
|
9
|
+
# "sherlock": {
|
|
10
|
+
# "display_name": "Sherlock",
|
|
11
11
|
# "local": true,
|
|
12
12
|
# "env": {
|
|
13
13
|
# "SOME_TOKEN": "token_abc...",
|
|
14
|
-
# "
|
|
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., "
|
|
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. "@
|
|
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
|
data/lib/brainiac/config.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "openssl"
|
|
|
5
5
|
require "open3"
|
|
6
6
|
require "fileutils"
|
|
7
7
|
require "logger"
|
|
8
|
+
require "securerandom"
|
|
8
9
|
require "net/http"
|
|
9
10
|
require "uri"
|
|
10
11
|
|
|
@@ -46,15 +47,6 @@ AI_AGENT_NAME = ENV.fetch("AI_AGENT_NAME", nil) || BRAINIAC_CONFIG["default_agen
|
|
|
46
47
|
MSG
|
|
47
48
|
end
|
|
48
49
|
|
|
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
50
|
LOG_LEVEL = ENV.fetch("LOG_LEVEL", "info").downcase
|
|
59
51
|
LOG = Logger.new($stdout)
|
|
60
52
|
LOG.level = case LOG_LEVEL
|
|
@@ -78,26 +70,6 @@ KNOWLEDGE_COLLECTION = "brainiac-knowledge"
|
|
|
78
70
|
|
|
79
71
|
ROLES_DIR = File.join(BRAINIAC_DIR, "roles")
|
|
80
72
|
|
|
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
73
|
NOTIFICATION_COMMAND = ENV.fetch("NOTIFICATION_COMMAND", nil)
|
|
102
74
|
|
|
103
75
|
# --- Projects ---
|
|
@@ -137,13 +109,6 @@ def reload_projects!(force: false)
|
|
|
137
109
|
LOG.info "Reloaded projects configuration: #{PROJECTS.keys.join(", ")}"
|
|
138
110
|
end
|
|
139
111
|
|
|
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
112
|
PROJECTS = load_projects_config
|
|
148
113
|
|
|
149
114
|
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,
|
|
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,
|
|
273
|
-
def update_cron_job(id, schedule: nil,
|
|
274
|
-
return { error: "No updates provided" } if schedule.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[:
|
|
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]
|
|
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:
|
|
347
|
-
notify_target: job[:notify_target]
|
|
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]
|
|
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]
|
|
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 =
|
|
393
|
-
notify_target = job[:notify_target]
|
|
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,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# Shared inline tag parsing for handler messages.
|
|
4
4
|
#
|
|
5
5
|
# Messages from any channel can contain inline tags like:
|
|
6
|
-
# [project:my-project], [opus], [effort:high], [cli:grok], [chat], [plan]
|
|
6
|
+
# [project:my-project], [opus], [effort:high], [cli:grok], [chat], [plan],
|
|
7
|
+
# [branch:feature-xyz], [workitem:wi-abc123]
|
|
7
8
|
#
|
|
8
9
|
# This module provides a single parser that extracts all tags and returns
|
|
9
10
|
# a structured result with the cleaned text.
|
|
@@ -31,9 +32,21 @@ def parse_inline_tags(text)
|
|
|
31
32
|
planning: false,
|
|
32
33
|
deploy_intent: nil,
|
|
33
34
|
worktree_override: nil,
|
|
35
|
+
work_item: nil,
|
|
36
|
+
branch_override: nil,
|
|
34
37
|
clean_text: text.dup
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
parse_value_tags(result)
|
|
41
|
+
parse_flag_tags(result)
|
|
42
|
+
parse_work_item_tags(result)
|
|
43
|
+
parse_model_tag(result)
|
|
44
|
+
|
|
45
|
+
result[:clean_text].strip!
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_value_tags(result)
|
|
37
50
|
# [project:my-project]
|
|
38
51
|
if (match = result[:clean_text].match(/\[project:(\S+)\]/i))
|
|
39
52
|
result[:project] = match[1]
|
|
@@ -52,6 +65,14 @@ def parse_inline_tags(text)
|
|
|
52
65
|
result[:clean_text].sub!(match[0], "")
|
|
53
66
|
end
|
|
54
67
|
|
|
68
|
+
# [deploy] or [deploy:dev01]
|
|
69
|
+
if (match = result[:clean_text].match(/\[deploy(?::([^\]]+))?\]/i))
|
|
70
|
+
result[:deploy_intent] = match[1]&.strip&.downcase || :auto
|
|
71
|
+
result[:clean_text].sub!(match[0], "")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_flag_tags(result)
|
|
55
76
|
# [chat], [question], [?]
|
|
56
77
|
if result[:clean_text].match?(/\[(chat|question|\?)\]/i)
|
|
57
78
|
result[:chat_mode] = true
|
|
@@ -59,23 +80,35 @@ def parse_inline_tags(text)
|
|
|
59
80
|
end
|
|
60
81
|
|
|
61
82
|
# [plan]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
return unless result[:clean_text].match?(/\[plan\]/i)
|
|
84
|
+
|
|
85
|
+
result[:planning] = true
|
|
86
|
+
result[:clean_text].sub!(/\[plan\]/i, "")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_work_item_tags(result)
|
|
90
|
+
# [worktree:branch-name] — legacy syntax, still supported
|
|
91
|
+
if (match = result[:clean_text].match(/\[worktree:([^\]]+)\]/))
|
|
92
|
+
result[:worktree_override] = match[1].strip
|
|
93
|
+
result[:clean_text].sub!(match[0], "")
|
|
65
94
|
end
|
|
66
95
|
|
|
67
|
-
# [
|
|
68
|
-
if (match = result[:clean_text].match(/\[
|
|
69
|
-
result[:
|
|
96
|
+
# [branch:branch-name] — preferred syntax for targeting a branch/worktree
|
|
97
|
+
if (match = result[:clean_text].match(/\[branch:([^\]]+)\]/i))
|
|
98
|
+
result[:branch_override] = match[1].strip
|
|
99
|
+
# Also set worktree_override for backward compat with plugins that read it
|
|
100
|
+
result[:worktree_override] ||= result[:branch_override]
|
|
70
101
|
result[:clean_text].sub!(match[0], "")
|
|
71
102
|
end
|
|
72
103
|
|
|
73
|
-
# [
|
|
74
|
-
if (match = result[:clean_text].match(/\[
|
|
75
|
-
result[:
|
|
104
|
+
# [workitem:wi-abc123] — target a specific work item by ID
|
|
105
|
+
if (match = result[:clean_text].match(/\[workitem:([^\]]+)\]/i))
|
|
106
|
+
result[:work_item] = match[1].strip
|
|
76
107
|
result[:clean_text].sub!(match[0], "")
|
|
77
108
|
end
|
|
109
|
+
end
|
|
78
110
|
|
|
111
|
+
def parse_model_tag(result)
|
|
79
112
|
# Model tag: any remaining [word] that isn't a known tag — detected separately
|
|
80
113
|
# because it depends on the project's allowed_models config. We just capture
|
|
81
114
|
# the raw match here for the caller to resolve.
|
|
@@ -83,7 +116,4 @@ def parse_inline_tags(text)
|
|
|
83
116
|
result[:model_tag] = match[1].downcase
|
|
84
117
|
result[:clean_text].sub!(match[0], "")
|
|
85
118
|
end
|
|
86
|
-
|
|
87
|
-
result[:clean_text].strip!
|
|
88
|
-
result
|
|
89
119
|
end
|