brainiac 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +382 -11
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +15 -38
  7. data/lib/brainiac/config.rb +40 -11
  8. data/lib/brainiac/handlers/discord/api.rb +196 -0
  9. data/lib/brainiac/handlers/discord/config.rb +134 -0
  10. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  11. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  12. data/lib/brainiac/handlers/discord/message.rb +933 -0
  13. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  14. data/lib/brainiac/handlers/discord.rb +14 -1892
  15. data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
  16. data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
  17. data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
  18. data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
  19. data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
  20. data/lib/brainiac/handlers/fizzy.rb +12 -1290
  21. data/lib/brainiac/handlers/github.rb +149 -212
  22. data/lib/brainiac/handlers/shared/git.rb +203 -0
  23. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  24. data/lib/brainiac/handlers/zoho.rb +79 -76
  25. data/lib/brainiac/helpers.rb +2 -121
  26. data/lib/brainiac/planning.rb +2 -2
  27. data/lib/brainiac/plugins.rb +129 -0
  28. data/lib/brainiac/restart.rb +94 -0
  29. data/lib/brainiac/routes/api.rb +427 -0
  30. data/lib/brainiac/version.rb +1 -1
  31. data/lib/brainiac/zoho_mail_api.rb +2 -1
  32. data/lib/brainiac.rb +7 -0
  33. data/monitor/daemon.rb +4 -27
  34. data/monitor/shared.rb +247 -0
  35. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
  36. data/monitor/waybar/setup.rb +232 -0
  37. data/monitor/waybar/status.rb +51 -0
  38. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  39. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  40. data/monitor/xbar/plugin.rb +155 -0
  41. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  42. data/receiver.rb +91 -450
  43. data/templates/brainiac.json.example +9 -0
  44. data/templates/cli-providers/kiro.json.example +8 -2
  45. data/templates/plugins.json.example +3 -0
  46. metadata +30 -20
  47. data/lib/user_registry.rb +0 -159
  48. data/monitor/menubar.rb +0 -295
  49. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  50. data/monitor/setup-waybar-deployments.rb +0 -96
  51. data/monitor/setup-waybar-module.rb +0 -113
  52. data/monitor/setup-xbar-plugin.rb +0 -35
  53. data/monitor/view-logs.rb +0 -119
  54. data/monitor/waybar-config-updater.rb +0 -56
  55. data/monitor/waybar-deployments.rb +0 -239
  56. data/monitor/waybar.rb +0 -146
  57. data/monitor/xbar.3s.rb +0 -179
  58. /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
  59. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  60. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
data/monitor/daemon.rb CHANGED
@@ -2,32 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Brainiac Monitor Daemon
5
- # Polls /api/status and exposes agent state via Unix socket for waybar
6
- # No longer triggers config updates - waybar module polls this socket directly
5
+ # Polls /api/status and exposes agent state via Unix socket for waybar/xbar.
6
+ # Modules (waybar/status.rb, xbar/plugin.rb) read from this socket.
7
7
 
8
- require "json"
9
- require "net/http"
10
- require "socket"
8
+ require "fileutils"
9
+ require_relative "shared"
11
10
 
12
- SOCKET_PATH = "/tmp/brainiac-monitor.sock"
13
- API_URL = "http://localhost:4567/api/status"
14
11
  POLL_INTERVAL = 2 # seconds
15
- CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
16
-
17
- # Load agent configuration from JSON
18
- def load_agent_config
19
- config = JSON.parse(File.read(CONFIG_PATH))
20
- agents = {}
21
- config["agents"].each do |agent|
22
- agents[agent["name"]] = { color: agent["color"], emoji: agent["emoji"] }
23
- end
24
- agents
25
- rescue StandardError => e
26
- warn "Failed to load waybar.json: #{e.message}"
27
- {}
28
- end
29
-
30
- AGENTS = load_agent_config.freeze
31
12
 
32
13
  @state = { sessions: [], count: 0, recent: [], last_update: nil }
33
14
 
@@ -67,12 +48,10 @@ def start_server
67
48
  server = UNIXServer.new(SOCKET_PATH)
68
49
  File.chmod(0o666, SOCKET_PATH)
69
50
 
70
- # Write PID file
71
51
  File.write("/tmp/brainiac-daemon.pid", Process.pid)
72
52
 
73
53
  puts "Monitor daemon started, socket: #{SOCKET_PATH}"
74
54
 
75
- # Start polling thread
76
55
  poller = Thread.new do
77
56
  loop do
78
57
  update_state
@@ -80,10 +59,8 @@ def start_server
80
59
  end
81
60
  end
82
61
 
83
- # Initial state fetch
84
62
  update_state
85
63
 
86
- # Accept client connections
87
64
  loop do
88
65
  client = server.accept
89
66
  Thread.new { handle_client(client) }
data/monitor/shared.rb ADDED
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared helpers for all monitor scripts (waybar, xbar, daemon).
4
+ #
5
+ # Provides: constants, config loading, state fetching, formatting utilities.
6
+ # Require this at the top of any monitor script.
7
+
8
+ require "json"
9
+ require "net/http"
10
+ require "socket"
11
+ require "uri"
12
+
13
+ # --- Constants ---
14
+
15
+ SOCKET_PATH = "/tmp/brainiac-monitor.sock"
16
+ API_URL = "http://localhost:4567/api/status"
17
+ SERVER_URL = "http://localhost:4567"
18
+ CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
19
+ BRAINIAC_DIR = File.expand_path("~/.brainiac")
20
+
21
+ # --- Agent Config ---
22
+
23
+ # Load agent configuration from ~/.brainiac/waybar.json.
24
+ # Returns { "galen" => { emoji: "🤖", color: "blue" }, ... }
25
+ def load_agent_config
26
+ config = JSON.parse(File.read(CONFIG_PATH, encoding: "utf-8"))
27
+ agents = {}
28
+ (config["agents"] || []).each do |agent|
29
+ agents[agent["name"].downcase] = { emoji: agent["emoji"], color: agent["color"] }
30
+ end
31
+ agents
32
+ rescue StandardError => e
33
+ warn "Failed to load waybar.json: #{e.message}"
34
+ {}
35
+ end
36
+
37
+ def load_monitor_config
38
+ return {} unless File.exist?(CONFIG_PATH)
39
+
40
+ JSON.parse(File.read(CONFIG_PATH, encoding: "utf-8"))
41
+ rescue StandardError
42
+ {}
43
+ end
44
+
45
+ DEFAULT_EMOJI = "❓"
46
+
47
+ # --- State Fetching ---
48
+
49
+ # Fetch state from daemon socket (fast, no HTTP overhead).
50
+ def fetch_state_from_socket
51
+ socket = UNIXSocket.new(SOCKET_PATH)
52
+ data = socket.read
53
+ socket.close
54
+ JSON.parse(data)
55
+ end
56
+
57
+ # Fetch state from brainiac HTTP API (fallback when daemon not running).
58
+ def fetch_state_from_api
59
+ uri = URI(API_URL)
60
+ response = Net::HTTP.get_response(uri)
61
+ return nil unless response.is_a?(Net::HTTPSuccess)
62
+
63
+ JSON.parse(response.body)
64
+ rescue StandardError
65
+ nil
66
+ end
67
+
68
+ # Fetch agent state — tries socket first, falls back to API.
69
+ def fetch_state
70
+ fetch_state_from_socket
71
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
72
+ fetch_state_from_api || { "sessions" => [], "count" => 0, "error" => "server not reachable" }
73
+ rescue StandardError => e
74
+ { "sessions" => [], "count" => 0, "error" => e.message }
75
+ end
76
+
77
+ # Fetch deployment state from API.
78
+ def fetch_deployments
79
+ uri = URI("#{SERVER_URL}/api/deployments")
80
+ response = Net::HTTP.get_response(uri)
81
+ JSON.parse(response.body)["deployments"] || []
82
+ rescue StandardError
83
+ nil
84
+ end
85
+
86
+ # --- Formatting ---
87
+
88
+ def format_elapsed(seconds)
89
+ return "#{seconds}s" if seconds < 60
90
+
91
+ minutes = seconds / 60
92
+ return "#{minutes}m" if minutes < 60
93
+
94
+ hours = minutes / 60
95
+ return "#{hours}h" if hours < 24
96
+
97
+ "#{hours / 24}d"
98
+ end
99
+
100
+ def format_context(card_key)
101
+ return "" unless card_key
102
+
103
+ if card_key.start_with?("discord-")
104
+ "Discord"
105
+ elsif card_key.start_with?("card-")
106
+ "##{card_key.split("-")[1]}"
107
+ else
108
+ card_key
109
+ end
110
+ end
111
+
112
+ def time_ago(iso_string)
113
+ return nil unless iso_string
114
+
115
+ seconds = (Time.now - Time.parse(iso_string)).to_i
116
+ "#{format_elapsed(seconds)} ago"
117
+ rescue StandardError
118
+ nil
119
+ end
120
+
121
+ # --- Log Preview ---
122
+
123
+ ANSI_REGEX = /\e\[[0-9;]*[a-zA-Z]|\e\[\?[0-9;]*[a-zA-Z]/
124
+ LOG_PREVIEW_LINES = 15
125
+ LOG_LINE_MAX = 80
126
+ LOG_FONT = "SFMono-Regular"
127
+ LOG_SIZE = 12
128
+
129
+ def tail_log(log_file, lines: LOG_PREVIEW_LINES)
130
+ return [] unless log_file && File.exist?(log_file)
131
+
132
+ raw = `tail -n 50 #{log_file.shellescape} 2>/dev/null`
133
+ raw.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
134
+ .lines
135
+ .map { |l| l.gsub(ANSI_REGEX, "").gsub(/[^[:print:]\t]/, "").strip }
136
+ .reject(&:empty?)
137
+ .last(lines)
138
+ rescue StandardError
139
+ []
140
+ end
141
+
142
+ def format_log_line(text)
143
+ text.length > LOG_LINE_MAX ? "#{text[0, LOG_LINE_MAX]}…" : text
144
+ end
145
+
146
+ # --- Deploy Helpers ---
147
+
148
+ DEPLOY_RECENT_WINDOW = 30 * 60 # 30 minutes
149
+
150
+ def deploy_dot_emoji(dep)
151
+ status = dep["last_deploy_status"]
152
+ if status == "deploying"
153
+ "🟠"
154
+ elsif status == "failed"
155
+ "💥"
156
+ elsif dep["status"] == "occupied"
157
+ deploy_time = dep["last_deploy_at"] || dep["deployed_at"]
158
+ recent = deploy_time && (Time.now - Time.parse(deploy_time)) < DEPLOY_RECENT_WINDOW
159
+ recent ? "🚀" : "🔴"
160
+ else
161
+ "🟢"
162
+ end
163
+ end
164
+
165
+ # Resolve worktree path for a card number from card_map.json or filesystem glob.
166
+ def resolve_worktree(card_number, glob_base: "~/Code")
167
+ # Try card_map.json first
168
+ card_map_path = File.join(BRAINIAC_DIR, "card_map.json")
169
+ if File.exist?(card_map_path)
170
+ card_map = begin
171
+ JSON.parse(File.read(card_map_path))
172
+ rescue StandardError
173
+ {}
174
+ end
175
+ entry = card_map.values.find { |e| e["number"].to_s == card_number.to_s }
176
+ return entry["worktree"] if entry && entry["worktree"] && File.directory?(entry["worktree"].to_s)
177
+ end
178
+
179
+ # Fallback: glob
180
+ matches = Dir.glob(File.expand_path("#{glob_base}/*fizzy-#{card_number}-*/"))
181
+ matches.find { |d| File.directory?(d) }
182
+ end
183
+
184
+ # Generate the bash deploy script with terraform lock retry logic.
185
+ def deploy_bash_script(env_key, worktree:, aws_profile: nil)
186
+ <<~BASH
187
+ cd #{worktree.shellescape}
188
+ #{"export AWS_PROFILE=#{aws_profile.shellescape}" if aws_profile}
189
+ echo "🚀 Deploying to #{env_key}..."
190
+ echo
191
+ logfile=$(mktemp)
192
+ ./scripts/deploy.sh #{env_key.shellescape} 2>&1 | tee "$logfile"
193
+ status=${PIPESTATUS[0]}
194
+ if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
195
+ echo
196
+ echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
197
+ echo
198
+ rm -f infrastructure/#{env_key.shellescape}/.terraform.lock.hcl
199
+ (cd infrastructure/#{env_key.shellescape} && terraform init -upgrade)
200
+ echo
201
+ echo "🔄 Retrying deploy..."
202
+ echo
203
+ ./scripts/deploy.sh #{env_key.shellescape} 2>&1
204
+ status=$?
205
+ fi
206
+ rm -f "$logfile"
207
+ echo
208
+ if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
209
+ echo "Press Enter to close..."
210
+ read
211
+ BASH
212
+ end
213
+
214
+ # Mark an environment as deploying via the brainiac API.
215
+ def mark_deploying_via_api(env_key, worktree:)
216
+ uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
217
+ req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
218
+ req.body = { worktree: worktree }.to_json
219
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
220
+ rescue StandardError
221
+ # Non-fatal — deploy proceeds even if server is unreachable
222
+ end
223
+
224
+ # Resolve AWS_PROFILE from deployments.json for an environment.
225
+ def resolve_aws_profile(env_key)
226
+ config_file = File.join(BRAINIAC_DIR, "deployments.json")
227
+ return nil unless File.exist?(config_file)
228
+
229
+ cfg = begin
230
+ JSON.parse(File.read(config_file))
231
+ rescue StandardError
232
+ {}
233
+ end
234
+ cfg.dig("environments", env_key, "aws_profile")
235
+ end
236
+
237
+ # --- Color ---
238
+
239
+ COLOR_MAP = {
240
+ "red" => "#ff5555", "green" => "#50fa7b", "blue" => "#8be9fd",
241
+ "yellow" => "#f1fa8c", "cyan" => "#8be9fd", "magenta" => "#ff79c6",
242
+ "purple" => "#bd93f9", "pink" => "#ff79c6", "white" => "#f8f8f2"
243
+ }.freeze
244
+
245
+ def hex_color(name)
246
+ COLOR_MAP[name] || name
247
+ end
@@ -2,18 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Brainiac Waybar Per-Environment Deploy Module
5
- # Usage: waybar-deploy-env.rb <env_key>
6
- # waybar-deploy-env.rb <env_key> --click
7
- # waybar-deploy-env.rb <env_key> --deploy
5
+ # Usage: deploy_env.rb <env_key>
6
+ # deploy_env.rb <env_key> --click
7
+ # deploy_env.rb <env_key> --deploy
8
8
 
9
- require "json"
10
- require "net/http"
11
9
  require "shellwords"
12
- require "uri"
13
10
  require "time"
14
-
15
- SERVER_URL = "http://localhost:4567"
16
- RECENT_WINDOW = 30 * 60
11
+ require_relative "../shared"
17
12
 
18
13
  env_key = ARGV.find { |a| !a.start_with?("--") }
19
14
  unless env_key
@@ -21,30 +16,7 @@ unless env_key
21
16
  exit
22
17
  end
23
18
 
24
- def fetch_deployments
25
- uri = URI("#{SERVER_URL}/api/deployments")
26
- response = Net::HTTP.get_response(uri)
27
- JSON.parse(response.body)["deployments"] || []
28
- rescue StandardError
29
- nil
30
- end
31
-
32
- def time_ago(iso_time)
33
- return nil unless iso_time
34
-
35
- seconds = (Time.now - Time.parse(iso_time)).to_i
36
- return "#{seconds}s ago" if seconds < 60
37
-
38
- minutes = seconds / 60
39
- return "#{minutes}m ago" if minutes < 60
40
-
41
- hours = minutes / 60
42
- "#{hours}h ago"
43
- end
44
-
45
19
  def resize_deploy_terminal
46
- # Shrink the deploy terminal to ~15% width after it tiles in at 50%
47
- # Calculates resize delta from monitor width dynamically
48
20
  script = "sleep 0.5 && " \
49
21
  'width=$(hyprctl monitors -j | ruby -rjson -e "puts JSON.parse(STDIN.read)[0][%q(width)]") && ' \
50
22
  "delta=$(( (width / 2) - (width * 15 / 100) )) && " \
@@ -58,8 +30,10 @@ def handle_click(env_key, deployment)
58
30
  if deployment["last_deploy_status"] == "failed" && deployment["last_deploy_log"]
59
31
  log = deployment["last_deploy_log"]
60
32
  if File.exist?(log.to_s)
61
- spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c",
62
- "echo '=== Deploy failure: #{deployment["label"] || env_key} ===' && echo && cat #{Shellwords.escape(log)} && echo && echo 'Press Enter to close...' && read",
33
+ label = deployment["label"] || env_key
34
+ cmd = "echo '=== Deploy failure: #{label} ===' && echo && " \
35
+ "cat #{Shellwords.escape(log)} && echo && echo 'Press Enter to close...' && read"
36
+ spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", cmd,
63
37
  %i[out err] => "/dev/null")
64
38
  resize_deploy_terminal
65
39
  return
@@ -79,63 +53,17 @@ def handle_deploy(env_key, deployment)
79
53
  end} 2>/dev/null`.strip
80
54
  return if card_number.empty?
81
55
 
82
- matches = Dir.glob(File.expand_path("~/Code/*fizzy-#{card_number}-*/"))
83
- worktree = matches.find { |d| File.directory?(d) }
56
+ worktree = resolve_worktree(card_number)
84
57
  unless worktree
85
58
  `timeout 10 zenity --error --text="No worktree found for card ##{card_number}" 2>/dev/null`
86
59
  return
87
60
  end
88
61
 
89
- # Resolve AWS_PROFILE from deployments config
90
- aws_profile = nil
91
- config_file = File.expand_path("~/.brainiac/deployments.json")
92
- if File.exist?(config_file)
93
- cfg = begin
94
- JSON.parse(File.read(config_file))
95
- rescue StandardError
96
- {}
97
- end
98
- aws_profile = cfg.dig("environments", env_key, "aws_profile")
99
- end
100
-
101
- deploy_script = <<~BASH
102
- cd #{Shellwords.escape(worktree)}
103
- #{"export AWS_PROFILE=#{Shellwords.escape(aws_profile)}" if aws_profile}
104
- echo "🚀 #{env_key} deploy in progress..."
105
- echo
106
- logfile=$(mktemp)
107
- ./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1 | tee "$logfile"
108
- status=${PIPESTATUS[0]}
109
- if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
110
- echo
111
- echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
112
- echo
113
- rm -f infrastructure/#{Shellwords.escape(env_key)}/.terraform.lock.hcl
114
- (cd infrastructure/#{Shellwords.escape(env_key)} && terraform init -upgrade)
115
- echo
116
- echo "🔄 Retrying deploy..."
117
- echo
118
- ./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1
119
- status=$?
120
- fi
121
- rm -f "$logfile"
122
- echo
123
- if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
124
- echo "Press Enter to close..."
125
- read
126
- BASH
127
-
128
- # Mark deploying via API so waybar turns orange immediately
129
- begin
130
- uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
131
- req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
132
- req.body = { worktree: worktree }.to_json
133
- Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
134
- rescue StandardError
135
- # Non-fatal — deploy proceeds even if server is unreachable
136
- end
62
+ aws_profile = resolve_aws_profile(env_key)
63
+ mark_deploying_via_api(env_key, worktree: worktree)
137
64
 
138
- spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", deploy_script, %i[out err] => "/dev/null")
65
+ script = deploy_bash_script(env_key, worktree: worktree, aws_profile: aws_profile)
66
+ spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", script, %i[out err] => "/dev/null")
139
67
  resize_deploy_terminal
140
68
  end
141
69
 
@@ -156,7 +84,7 @@ def generate_output(env_key)
156
84
 
157
85
  if d["status"] == "occupied"
158
86
  deploy_time = d["last_deploy_at"] || d["deployed_at"]
159
- recent = deploy_time && (Time.now - Time.parse(deploy_time)) < RECENT_WINDOW
87
+ recent = deploy_time && (Time.now - Time.parse(deploy_time)) < DEPLOY_RECENT_WINDOW
160
88
  status = d["last_deploy_status"]
161
89
 
162
90
  if status == "deploying"
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # One-time setup: installs Brainiac modules into waybar config.
5
+ #
6
+ # Adds:
7
+ # - Agent session status module (custom/brainiac)
8
+ # - Per-environment deploy dots (custom/brainiac-deploy-<env>)
9
+ #
10
+ # Usage: ruby monitor/waybar/setup.rb
11
+
12
+ require "json"
13
+ require "fileutils"
14
+
15
+ WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
16
+ WAYBAR_STYLE = File.expand_path("~/.config/waybar/style.css")
17
+ BRAINIAC_DIR = File.expand_path("~/.brainiac")
18
+ DEPLOYMENTS_CONFIG = File.join(BRAINIAC_DIR, "deployments.json")
19
+
20
+ # Wrapper scripts go to ~/.brainiac/bin/ and resolve from /api/status (server_root field)
21
+ # Falls back to ~/.brainiac/server.root file for cold-start scenarios
22
+ WRAPPER_DIR = File.join(BRAINIAC_DIR, "bin")
23
+ FileUtils.mkdir_p(WRAPPER_DIR)
24
+
25
+ def load_waybar_config
26
+ content = File.read(WAYBAR_CONFIG)
27
+ json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
28
+ JSON.parse(json_content)
29
+ end
30
+
31
+ def save_waybar_config(config)
32
+ File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
33
+ end
34
+
35
+ # --- Create wrapper scripts ---
36
+
37
+ # Status wrapper
38
+ status_wrapper = File.join(WRAPPER_DIR, "waybar-status")
39
+ File.write(status_wrapper, <<~SCRIPT)
40
+ #!/usr/bin/env ruby
41
+ require "json"
42
+ require "net/http"
43
+ require "uri"
44
+
45
+ def resolve_server_root
46
+ # Primary: query the running server's /api/status endpoint
47
+ uri = URI("http://localhost:4567/api/status")
48
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
49
+ if response.is_a?(Net::HTTPSuccess)
50
+ data = JSON.parse(response.body)
51
+ root = data["server_root"]
52
+ return root if root && File.directory?(root)
53
+ end
54
+ # Fallback: read the file (cold-start or server returned no root)
55
+ root_file = File.expand_path("~/.brainiac/server.root")
56
+ File.read(root_file).strip if File.exist?(root_file)
57
+ rescue StandardError
58
+ # Fallback: read the file (server unreachable)
59
+ root_file = File.expand_path("~/.brainiac/server.root")
60
+ File.exist?(root_file) ? File.read(root_file).strip : nil
61
+ end
62
+
63
+ server_root = resolve_server_root
64
+ if server_root
65
+ script = File.join(server_root, "monitor", "waybar", "status.rb")
66
+ if File.exist?(script)
67
+ load script
68
+ exit
69
+ end
70
+ end
71
+ puts({ text: "⚠️", tooltip: "Brainiac server root not found", class: "error" }.to_json)
72
+ SCRIPT
73
+ File.chmod(0o755, status_wrapper)
74
+
75
+ # Log viewer wrapper
76
+ logs_wrapper = File.join(WRAPPER_DIR, "waybar-logs")
77
+ File.write(logs_wrapper, <<~SCRIPT)
78
+ #!/usr/bin/env ruby
79
+ require "json"
80
+ require "net/http"
81
+ require "uri"
82
+
83
+ def resolve_server_root
84
+ uri = URI("http://localhost:4567/api/status")
85
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
86
+ if response.is_a?(Net::HTTPSuccess)
87
+ data = JSON.parse(response.body)
88
+ root = data["server_root"]
89
+ return root if root && File.directory?(root)
90
+ end
91
+ root_file = File.expand_path("~/.brainiac/server.root")
92
+ File.read(root_file).strip if File.exist?(root_file)
93
+ rescue StandardError
94
+ root_file = File.expand_path("~/.brainiac/server.root")
95
+ File.exist?(root_file) ? File.read(root_file).strip : nil
96
+ end
97
+
98
+ server_root = resolve_server_root
99
+ if server_root
100
+ script = File.join(server_root, "monitor", "waybar", "view_logs.rb")
101
+ exec("ruby", script) if File.exist?(script)
102
+ end
103
+ warn "Brainiac server root not found"
104
+ SCRIPT
105
+ File.chmod(0o755, logs_wrapper)
106
+
107
+ # Deploy env wrapper
108
+ deploy_wrapper = File.join(WRAPPER_DIR, "waybar-deploy-env")
109
+ File.write(deploy_wrapper, <<~SCRIPT)
110
+ #!/usr/bin/env ruby
111
+ require "json"
112
+ require "net/http"
113
+ require "uri"
114
+
115
+ def resolve_server_root
116
+ uri = URI("http://localhost:4567/api/status")
117
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
118
+ if response.is_a?(Net::HTTPSuccess)
119
+ data = JSON.parse(response.body)
120
+ root = data["server_root"]
121
+ return root if root && File.directory?(root)
122
+ end
123
+ root_file = File.expand_path("~/.brainiac/server.root")
124
+ File.read(root_file).strip if File.exist?(root_file)
125
+ rescue StandardError
126
+ root_file = File.expand_path("~/.brainiac/server.root")
127
+ File.exist?(root_file) ? File.read(root_file).strip : nil
128
+ end
129
+
130
+ server_root = resolve_server_root
131
+ if server_root
132
+ script = File.join(server_root, "monitor", "waybar", "deploy_env.rb")
133
+ if File.exist?(script)
134
+ load script
135
+ exit
136
+ end
137
+ end
138
+ puts({ text: "", tooltip: "Brainiac server root not found", class: "error" }.to_json)
139
+ SCRIPT
140
+ File.chmod(0o755, deploy_wrapper)
141
+
142
+ puts "✓ Created wrapper scripts in #{WRAPPER_DIR}"
143
+
144
+ # --- Update waybar config ---
145
+
146
+ config = load_waybar_config
147
+
148
+ # Clean up old brainiac modules from all positions
149
+ %w[modules-left modules-center modules-right].each do |pos|
150
+ next unless config[pos]
151
+
152
+ config[pos].reject! { |m| m.to_s.include?("brainiac") }
153
+ end
154
+ config.each_key { |key| config.delete(key) if key.to_s.include?("brainiac") }
155
+
156
+ # Add agent session module to modules-center
157
+ config["modules-center"] ||= []
158
+ config["modules-center"] << "custom/brainiac"
159
+
160
+ config["custom/brainiac"] = {
161
+ "exec" => status_wrapper,
162
+ "return-type" => "json",
163
+ "interval" => 3,
164
+ "format" => "{}",
165
+ "tooltip" => true,
166
+ "on-click" => logs_wrapper
167
+ }
168
+
169
+ # Add per-environment deploy modules (if deployments.json exists)
170
+ if File.exist?(DEPLOYMENTS_CONFIG)
171
+ deployments = JSON.parse(File.read(DEPLOYMENTS_CONFIG))
172
+ envs = (deployments["environments"] || {}).keys
173
+
174
+ center = config["modules-center"]
175
+ brainiac_idx = center.index("custom/brainiac") || center.length
176
+
177
+ envs.each_with_index do |env, i|
178
+ mod_name = "custom/brainiac-deploy-#{env}"
179
+ center.insert(brainiac_idx + i, mod_name)
180
+
181
+ config[mod_name] = {
182
+ "exec" => "#{deploy_wrapper} #{env}",
183
+ "return-type" => "json",
184
+ "interval" => 30,
185
+ "format" => "{}",
186
+ "tooltip" => true,
187
+ "escape" => false,
188
+ "on-click" => "#{deploy_wrapper} #{env} --click",
189
+ "on-click-right" => "#{deploy_wrapper} #{env} --deploy"
190
+ }
191
+ end
192
+
193
+ puts "✓ Added #{envs.size} deploy environment module(s): #{envs.join(", ")}"
194
+ end
195
+
196
+ save_waybar_config(config)
197
+ puts "✓ Updated waybar config at #{WAYBAR_CONFIG}"
198
+
199
+ # --- Update CSS ---
200
+
201
+ style = File.exist?(WAYBAR_STYLE) ? File.read(WAYBAR_STYLE) : ""
202
+
203
+ unless style.include?("#custom-brainiac")
204
+ css = <<~CSS
205
+
206
+ /* Brainiac agent session module */
207
+ #custom-brainiac {
208
+ padding-right: 100px;
209
+ }
210
+
211
+ /* Brainiac per-environment deploy dots */
212
+ [id^="custom-brainiac-deploy-"] {
213
+ font-size: 28px;
214
+ padding: 0 6px;
215
+ border-radius: 8px;
216
+ border: 2px solid transparent;
217
+ }
218
+
219
+ [id^="custom-brainiac-deploy-"].deploy-recent {
220
+ border: 2px solid #4488ff;
221
+ }
222
+
223
+ [id^="custom-brainiac-deploy-"].deploy-failed {
224
+ border: 2px solid #ff4444;
225
+ }
226
+ CSS
227
+ File.write(WAYBAR_STYLE, "#{style.strip}\n#{css}")
228
+ puts "✓ Added brainiac styles to #{WAYBAR_STYLE}"
229
+ end
230
+
231
+ puts ""
232
+ puts "Restart waybar to apply: omarchy restart waybar"