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/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
@@ -74,68 +48,22 @@ def handle_deploy(env_key, deployment)
74
48
  return unless deployment
75
49
 
76
50
  prefill = deployment["status"] == "occupied" && deployment["card_number"] ? deployment["card_number"].to_s : ""
77
- card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Fizzy card number:"#{unless prefill.empty?
78
- " --entry-text=#{Shellwords.escape(prefill)}"
79
- end} 2>/dev/null`.strip
51
+ card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Card number:"#{unless prefill.empty?
52
+ " --entry-text=#{Shellwords.escape(prefill)}"
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"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Brainiac Waybar Status Module
5
+ # Reads from daemon socket and outputs JSON for waybar (agent sessions).
6
+
7
+ require_relative "../shared"
8
+
9
+ AGENTS = load_agent_config.freeze
10
+ INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
11
+
12
+ def escape_pango(str)
13
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
14
+ end
15
+
16
+ def generate_output
17
+ state = fetch_state
18
+
19
+ return { text: "⚠️", tooltip: "Brainiac Error: #{escape_pango(state["error"])}", class: "error" } if state["error"]
20
+
21
+ sessions = state["sessions"] || []
22
+
23
+ return { text: "💤", tooltip: "No active agent sessions", class: "idle" } if sessions.empty?
24
+
25
+ text = sessions.map { |s| AGENTS.dig(s["agent"]&.downcase, :emoji) || DEFAULT_EMOJI }.join(" ")
26
+
27
+ tooltip_lines = sessions.map do |s|
28
+ agent = s["agent"]
29
+ emoji = AGENTS.dig(agent&.downcase, :emoji) || DEFAULT_EMOJI
30
+ elapsed = format_elapsed(s["elapsed_seconds"] || 0)
31
+ context = format_context(s["card_key"])
32
+
33
+ lines = ["#{emoji} #{agent}: #{context} (#{elapsed})"]
34
+
35
+ (s["children"] || []).each do |c|
36
+ cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
37
+ cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
38
+ next if INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
39
+
40
+ lines << " └ #{escape_pango(cmd_short)} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]"
41
+ end
42
+
43
+ lines.join("\n")
44
+ end
45
+
46
+ tooltip_lines << "\n[Click to manage]"
47
+
48
+ { text: text, tooltip: tooltip_lines.join("\n"), class: "working" }
49
+ end
50
+
51
+ puts generate_output.to_json