brainiac 0.0.17 → 0.0.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: faf9d51fb98a0185bdf17224d0adc4539ec4b0584194ad4c67248f6092fd97a4
4
- data.tar.gz: 15ad522a423295771f9f55847b2a07ea207f28709dab653896dd233f5888b8fd
3
+ metadata.gz: 3a5e88ff667021341d3ff4bd4da63212b3918b8c210d276c3c9d0f00ff97e121
4
+ data.tar.gz: ba02b2ab6f6e0e519ef00887da6f53fb6be2dd234fb8db30bc5bc7c9896433b5
5
5
  SHA512:
6
- metadata.gz: 0c02497473d20acb2e64150a502cae30bad9e031da305be68655ee1b3f9854a99d734c93e887c2474927e7a63f85836c254cd1b14d5208778635c6940171be99
7
- data.tar.gz: 118a28a54a16e21cf20f0199ed36dace15e82dbae60d9cab2c090a22241fa6eb7a7a5d2dee53763b37a13d43e9e9a3855d60cc8868ecd30fa95a6fe045fdad0c
6
+ metadata.gz: 013f765fe110ad69fa3522ba45d46f3200f8f34bad4da62cc3df78b751246d52056b226005ecfce2cda99fbfb77e82230c1e6b679a5525d49b471770776c3249
7
+ data.tar.gz: b206d36e38e5fd284a8e276109e1a5656986f954f87e393cefa7ef77a0b1f0226b96fc50c80027e00ac66e60dc64107560fbe64d77d3a84198adb673cd1ca50c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.0.17)
4
+ brainiac (0.0.18)
5
5
  puma (~> 7.2)
6
6
  rackup (~> 2.3)
7
7
  sinatra (~> 4.1)
@@ -84,7 +84,7 @@ DEPENDENCIES
84
84
  CHECKSUMS
85
85
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
86
86
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
87
- brainiac (0.0.17)
87
+ brainiac (0.0.18)
88
88
  json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
89
89
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
90
90
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
data/bin/brainiac CHANGED
@@ -1162,7 +1162,7 @@ when "config"
1162
1162
 
1163
1163
  configure_server(options)
1164
1164
 
1165
- when "server", "s"
1165
+ when "server", "s", "start"
1166
1166
  OptionParser.new do |opts|
1167
1167
  opts.banner = "Usage: brainiac server [options]"
1168
1168
  opts.on("-d", "--daemon", "Run server in background (detached)")
@@ -3138,6 +3138,7 @@ when "help", "--help", "-h", nil
3138
3138
  Usage:
3139
3139
  brainiac setup Bootstrap a new machine (deps, dirs, configs)
3140
3140
  brainiac server [options] Start the Brainiac webhook server (foreground)
3141
+ brainiac start [options] Alias for 'brainiac server'
3141
3142
  brainiac stop Stop the running server
3142
3143
  brainiac restart Restart the server
3143
3144
  brainiac logs Tail the server log
@@ -3217,6 +3218,7 @@ when "help", "--help", "-h", nil
3217
3218
  Examples:
3218
3219
  # Start the server in the foreground (like rails server)
3219
3220
  brainiac server
3221
+ brainiac start
3220
3222
  brainiac s
3221
3223
  #{" "}
3222
3224
  # Register current directory
@@ -9,7 +9,7 @@ _brainiac() {
9
9
  local brainiac_dir="${BRAINIAC_DIR:-$HOME/.brainiac}"
10
10
 
11
11
  # Top-level commands (built-in + installed plugins)
12
- local commands="server stop restart logs status register unregister list show brain cron provider role agent config path version help setup projects card-map handler plugin install uninstall plugins"
12
+ local commands="server start stop restart logs status register unregister list show brain cron provider role agent config path version help setup projects card-map handler plugin install uninstall plugins"
13
13
 
14
14
  # Add installed plugin names as top-level commands
15
15
  if [[ -f "$brainiac_dir/plugins.json" ]]; then
@@ -254,6 +254,93 @@ def register_work_item(branch:, worktree: nil, project: nil, agent: nil, source:
254
254
  work_item_id
255
255
  end
256
256
 
257
+ # Update dispatch overrides on a work item (cli_provider, model, effort).
258
+ # Only non-nil values are stored; nil values are removed from existing overrides.
259
+ # Returns true if the work item was found and updated, false otherwise.
260
+ def update_work_item_overrides(cli_provider: nil, model: nil, effort: nil, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
261
+ map = load_work_item_map
262
+
263
+ target_id = work_item_id
264
+ unless target_id
265
+ map.each do |wid, info|
266
+ if info.is_a?(Hash) && info["branch"] == branch
267
+ target_id = wid
268
+ break
269
+ end
270
+ end
271
+ end
272
+
273
+ return false unless target_id && map[target_id]
274
+
275
+ overrides = map[target_id]["overrides"] || {}
276
+ overrides["cli_provider"] = cli_provider if cli_provider
277
+ overrides["model"] = model if model
278
+ overrides["effort"] = effort if effort
279
+ overrides.compact!
280
+
281
+ if overrides.empty?
282
+ map[target_id].delete("overrides")
283
+ else
284
+ map[target_id]["overrides"] = overrides
285
+ end
286
+
287
+ save_work_item_map(map)
288
+ LOG.info "[WorkItem] Updated overrides for #{target_id}: #{overrides}" unless overrides.empty?
289
+ true
290
+ end
291
+
292
+ # Retrieve dispatch overrides for a work item.
293
+ # Returns a hash (may be empty) with keys like "cli_provider", "model", "effort".
294
+ def work_item_overrides_for(work_item_id: nil, branch: nil)
295
+ map = load_work_item_map
296
+
297
+ target_id = work_item_id
298
+ unless target_id
299
+ map.each do |wid, info|
300
+ if info.is_a?(Hash) && info["branch"] == branch
301
+ target_id = wid
302
+ break
303
+ end
304
+ end
305
+ end
306
+
307
+ return {} unless target_id && map[target_id]
308
+
309
+ map[target_id]["overrides"] || {}
310
+ end
311
+
312
+ # Resolve dispatch overrides for a work item, merging stored overrides with
313
+ # inline tags from the current message. Inline tags take priority and are
314
+ # persisted for future dispatches.
315
+ #
316
+ # Parameters:
317
+ # work_item_id: or branch: — identifies the work item
318
+ # inline_cli_provider: — [cli:X] from current message (nil if not specified)
319
+ # inline_model: — resolved model from current message (nil if not specified)
320
+ # inline_effort: — [effort:X] from current message (nil if not specified)
321
+ #
322
+ # Returns: { cli_provider: String|nil, model: String|nil, effort: String|nil }
323
+ def resolve_work_item_overrides(work_item_id: nil, branch: nil, inline_cli_provider: nil, inline_model: nil, inline_effort: nil)
324
+ stored = work_item_overrides_for(work_item_id: work_item_id, branch: branch)
325
+
326
+ # Inline tags override stored values
327
+ resolved = {
328
+ cli_provider: inline_cli_provider || stored["cli_provider"],
329
+ model: inline_model || stored["model"],
330
+ effort: inline_effort || stored["effort"]
331
+ }
332
+
333
+ # Persist any new inline overrides to the work item for future dispatches
334
+ new_overrides = {}
335
+ new_overrides[:cli_provider] = inline_cli_provider if inline_cli_provider
336
+ new_overrides[:model] = inline_model if inline_model
337
+ new_overrides[:effort] = inline_effort if inline_effort
338
+
339
+ update_work_item_overrides(work_item_id: work_item_id, branch: branch, **new_overrides) if new_overrides.any? && (work_item_id || branch)
340
+
341
+ resolved
342
+ end
343
+
257
344
  # Add or update a source on an existing work item.
258
345
  # Returns true if the work item was found and updated, false otherwise.
259
346
  def register_work_item_source(source:, source_data:, work_item_id: nil, branch: nil) # rubocop:disable Naming/PredicateMethod
@@ -227,7 +227,8 @@ end
227
227
  # Lean prompt for resumed sessions. The previous session already has the full context
228
228
  # (role, persona, knowledge, core instructions, channel prompts). We only send the new
229
229
  # comment and any fresh card context so the agent knows what changed.
230
- def render_resume_prompt(comment_body:, comment_creator:, comment_id:, card_number: nil, agent_name: AI_AGENT_NAME)
230
+ def render_resume_prompt(comment_body:, comment_creator:, comment_id:, card_number: nil, agent_name: AI_AGENT_NAME,
231
+ response_destination: nil)
231
232
  # Touch memory file (same as render_prompt does)
232
233
  memory_dir = memory_dir_for(agent_name)
233
234
  card_id = card_number || "unknown"
@@ -247,6 +248,10 @@ def render_resume_prompt(comment_body:, comment_creator:, comment_id:, card_numb
247
248
  lines << ""
248
249
  lines << "---"
249
250
  lines << "Respond to this comment. All your previous instructions still apply."
251
+ if response_destination
252
+ lines << ""
253
+ lines << "**Response destination: #{response_destination}**"
254
+ end
250
255
 
251
256
  lines.join("\n")
252
257
  end
@@ -25,6 +25,7 @@ def format_active_sessions
25
25
  started_at: info[:started_at].iso8601,
26
26
  elapsed_seconds: (Time.now - info[:started_at]).to_i,
27
27
  log_file: info[:log_file], alive: true,
28
+ channel_id: info[:channel_id],
28
29
  children: child_processes_for(info[:pid])
29
30
  }
30
31
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.17"
5
+ VERSION = "0.0.18"
6
6
  end
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Brainiac Waybar Per-Session Module
5
+ # Each instance represents one session slot.
6
+ #
7
+ # Usage: session_slot.rb <index> → JSON output for waybar
8
+ # session_slot.rb <index> --tail → Left-click: tail the log
9
+ # session_slot.rb <index> --manage → Right-click: kill menu
10
+ # session_slot.rb <index> --thread → Middle-click: open Discord thread
11
+
12
+ require "shellwords"
13
+ require "time"
14
+ require_relative "../shared"
15
+
16
+ AGENTS = load_agent_config.freeze
17
+ INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
18
+ DISCORD_CONFIG_FILE = File.join(BRAINIAC_DIR, "discord.json")
19
+
20
+ index = ARGV.find { |a| !a.start_with?("--") }&.to_i
21
+ unless index
22
+ puts({ text: "", tooltip: "", class: "" }.to_json)
23
+ exit
24
+ end
25
+
26
+ def load_discord_guild_id
27
+ return nil unless File.exist?(DISCORD_CONFIG_FILE)
28
+
29
+ config = JSON.parse(File.read(DISCORD_CONFIG_FILE))
30
+ config["guild_id"]
31
+ rescue JSON::ParserError
32
+ nil
33
+ end
34
+
35
+ def escape_pango(str)
36
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
37
+ end
38
+
39
+ def find_terminal
40
+ %w[alacritty kitty gnome-terminal xterm].find { |t| system("which #{t} > /dev/null 2>&1") }
41
+ end
42
+
43
+ def open_log(log_file)
44
+ return unless log_file && File.exist?(log_file)
45
+
46
+ terminal = find_terminal
47
+ case terminal
48
+ when "alacritty"
49
+ spawn("alacritty", "-e", "tail", "-f", log_file, %i[out err] => "/dev/null")
50
+ when "kitty"
51
+ spawn("kitty", "tail", "-f", log_file, %i[out err] => "/dev/null")
52
+ when "gnome-terminal"
53
+ spawn("gnome-terminal", "--", "tail", "-f", log_file, %i[out err] => "/dev/null")
54
+ else
55
+ spawn("xterm", "-e", "tail", "-f", log_file, %i[out err] => "/dev/null")
56
+ end
57
+ end
58
+
59
+ def handle_tail(session)
60
+ return unless session
61
+
62
+ open_log(session["log_file"])
63
+ end
64
+
65
+ def handle_manage(session)
66
+ return unless session
67
+
68
+ entries = []
69
+ agent = session["agent"]
70
+ elapsed = format_elapsed(session["elapsed_seconds"] || 0)
71
+ context = format_context(session["card_key"])
72
+ emoji = AGENTS.dig(agent&.downcase, :emoji) || DEFAULT_EMOJI
73
+
74
+ entries << { display: "#{emoji} #{agent}: #{context} (#{elapsed})", type: :log, log: session["log_file"] }
75
+ entries << { display: " ⛔ Kill session", type: :kill_session, card_key: session["card_key"], agent: agent }
76
+
77
+ (session["children"] || []).each do |c|
78
+ cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
79
+ cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
80
+ next if INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
81
+
82
+ entries << {
83
+ display: " └ 🔪 Kill: #{cmd_short} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]",
84
+ type: :kill_child, pid: c["pid"], cmd: cmd_short
85
+ }
86
+ end
87
+
88
+ launcher = %w[rofi fuzzel wofi zenity fzf].find { |cmd| system("which #{cmd} > /dev/null 2>&1") }
89
+ unless launcher
90
+ system("notify-send", "Brainiac", "No menu launcher found")
91
+ return
92
+ end
93
+
94
+ menu_text = entries.map { |e| e[:display] }.join("\n")
95
+ selected_line = case launcher
96
+ when "rofi"
97
+ IO.popen(%w[rofi -dmenu -i -p] + ["Manage"], "r+") do |io|
98
+ io.puts menu_text
99
+ io.close_write
100
+ io.read.strip
101
+ end
102
+ when "fuzzel"
103
+ IO.popen(%w[fuzzel --dmenu --prompt] + ["Manage: "], "r+") do |io|
104
+ io.puts menu_text
105
+ io.close_write
106
+ io.read.strip
107
+ end
108
+ when "wofi"
109
+ IO.popen(%w[wofi --dmenu --prompt] + ["Manage"], "r+") do |io|
110
+ io.puts menu_text
111
+ io.close_write
112
+ io.read.strip
113
+ end
114
+ when "zenity"
115
+ IO.popen(["zenity", "--list", "--title", "Manage", "--column", "Action", "--width", "500", "--height", "300"], "r+",
116
+ err: "/dev/null") do |io|
117
+ entries.each { |e| io.puts e[:display] }
118
+ io.close_write
119
+ io.read.strip
120
+ end
121
+ when "fzf"
122
+ `echo "#{menu_text}" | fzf --prompt="Manage: "`.strip
123
+ end
124
+
125
+ return if selected_line.to_s.empty?
126
+
127
+ selected = entries.find { |e| e[:display].strip == selected_line.strip }
128
+ return unless selected
129
+
130
+ case selected[:type]
131
+ when :log
132
+ open_log(selected[:log])
133
+ when :kill_session
134
+ uri = URI("#{SERVER_URL}/api/sessions/kill/#{selected[:card_key]}")
135
+ Net::HTTP.post(uri, "", { "Content-Type" => "application/json" })
136
+ system("notify-send", "Brainiac", "Killed session: #{selected[:agent]}")
137
+ when :kill_child
138
+ begin
139
+ Process.kill("TERM", selected[:pid])
140
+ rescue StandardError
141
+ nil
142
+ end
143
+ Thread.new do
144
+ sleep 3
145
+ begin
146
+ Process.kill("KILL", selected[:pid])
147
+ rescue StandardError
148
+ nil
149
+ end
150
+ end
151
+ system("notify-send", "Brainiac", "Killed #{selected[:cmd]} (PID #{selected[:pid]})")
152
+ end
153
+ end
154
+
155
+ def handle_thread(session)
156
+ return unless session
157
+
158
+ guild_id = load_discord_guild_id
159
+ unless guild_id
160
+ system("notify-send", "Brainiac", "No guild_id in discord.json")
161
+ return
162
+ end
163
+
164
+ unless session["card_key"].to_s.start_with?("discord-")
165
+ system("notify-send", "Brainiac", "Not a Discord session (#{session["agent"]})")
166
+ return
167
+ end
168
+
169
+ thread_id = session["channel_id"]
170
+ unless thread_id
171
+ # Fallback: parse from card_key
172
+ parts = session["card_key"].to_s.split("-")
173
+ numeric_parts = parts.grep(/\A\d{15,}\z/)
174
+ thread_id = numeric_parts.first
175
+ end
176
+
177
+ unless thread_id
178
+ system("notify-send", "Brainiac", "Cannot determine thread ID")
179
+ return
180
+ end
181
+
182
+ url = "https://discord.com/channels/#{guild_id}/#{thread_id}"
183
+ spawn("xdg-open", url, %i[out err] => "/dev/null")
184
+ end
185
+
186
+ def generate_output(index)
187
+ state = fetch_state
188
+
189
+ if state["error"]
190
+ puts({ text: "", tooltip: "", class: "" }.to_json) if index.positive?
191
+ puts({ text: "⚠️", tooltip: "Brainiac Error: #{escape_pango(state["error"])}", class: "error" }.to_json) if index.zero?
192
+ return
193
+ end
194
+
195
+ sessions = state["sessions"] || []
196
+
197
+ if index.zero? && sessions.empty?
198
+ puts({ text: "💤", tooltip: "No active agent sessions", class: "idle" }.to_json)
199
+ return
200
+ end
201
+
202
+ session = sessions[index]
203
+ unless session
204
+ puts({ text: "", tooltip: "", class: "" }.to_json)
205
+ return
206
+ end
207
+
208
+ agent = session["agent"]
209
+ emoji = AGENTS.dig(agent&.downcase, :emoji) || DEFAULT_EMOJI
210
+ elapsed = format_elapsed(session["elapsed_seconds"] || 0)
211
+ context = format_context(session["card_key"])
212
+
213
+ tooltip_lines = ["#{emoji} #{agent}: #{context} (#{elapsed})"]
214
+
215
+ (session["children"] || []).each do |c|
216
+ cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
217
+ cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
218
+ next if INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
219
+
220
+ tooltip_lines << " └ #{escape_pango(cmd_short)} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]"
221
+ end
222
+
223
+ tooltip_lines << "\nL-click: tail log | R-click: manage | M-click: open thread"
224
+
225
+ puts({ text: emoji, tooltip: tooltip_lines.join("\n"), class: "working" }.to_json)
226
+ end
227
+
228
+ # --- Main ---
229
+
230
+ state = fetch_state
231
+ sessions = state["sessions"] || []
232
+ session = sessions[index]
233
+
234
+ if ARGV.include?("--tail")
235
+ handle_tail(session)
236
+ elsif ARGV.include?("--manage")
237
+ handle_manage(session)
238
+ elsif ARGV.include?("--thread")
239
+ handle_thread(session)
240
+ else
241
+ generate_output(index)
242
+ end
@@ -4,7 +4,7 @@
4
4
  # One-time setup: installs Brainiac modules into waybar config.
5
5
  #
6
6
  # Adds:
7
- # - Agent session status module (custom/brainiac)
7
+ # - Per-session slot modules (custom/brainiac-session-0 through N)
8
8
  # - Per-environment deploy dots (custom/brainiac-deploy-<env>)
9
9
  #
10
10
  # Usage: ruby monitor/waybar/setup.rb
@@ -16,12 +16,20 @@ WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
16
16
  WAYBAR_STYLE = File.expand_path("~/.config/waybar/style.css")
17
17
  BRAINIAC_DIR = File.expand_path("~/.brainiac")
18
18
  DEPLOYMENTS_CONFIG = File.join(BRAINIAC_DIR, "deployments.json")
19
+ WAYBAR_JSON = File.join(BRAINIAC_DIR, "waybar.json")
20
+
21
+ # Max concurrent agent sessions to show (each gets its own clickable module)
22
+ MAX_SESSION_SLOTS = 8
19
23
 
20
24
  # 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
25
+ # Falls back to the repo path where setup.rb was run from (handles gem version mismatch)
22
26
  WRAPPER_DIR = File.join(BRAINIAC_DIR, "bin")
23
27
  FileUtils.mkdir_p(WRAPPER_DIR)
24
28
 
29
+ # The directory where this setup script lives — used as fallback when the server_root
30
+ # (gem install) doesn't have newer files yet
31
+ SETUP_SOURCE_ROOT = File.expand_path("../..", __dir__)
32
+
25
33
  def load_waybar_config
26
34
  content = File.read(WAYBAR_CONFIG)
27
35
  json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
@@ -32,18 +40,15 @@ def save_waybar_config(config)
32
40
  File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
33
41
  end
34
42
 
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
43
+ # Shared resolver code used by all wrapper scripts
44
+ RESOLVER_CODE = <<~RUBY
41
45
  require "json"
42
46
  require "net/http"
43
47
  require "uri"
44
48
 
49
+ SETUP_SOURCE_ROOT = "#{SETUP_SOURCE_ROOT}"
50
+
45
51
  def resolve_server_root
46
- # Primary: query the running server's /api/status endpoint
47
52
  uri = URI("http://localhost:4567/api/status")
48
53
  response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
49
54
  if response.is_a?(Net::HTTPSuccess)
@@ -51,56 +56,48 @@ File.write(status_wrapper, <<~SCRIPT)
51
56
  root = data["server_root"]
52
57
  return root if root && File.directory?(root)
53
58
  end
54
- # Fallback: read the file (cold-start or server returned no root)
55
59
  root_file = File.expand_path("~/.brainiac/server.root")
56
60
  File.read(root_file).strip if File.exist?(root_file)
57
61
  rescue StandardError
58
- # Fallback: read the file (server unreachable)
59
62
  root_file = File.expand_path("~/.brainiac/server.root")
60
63
  File.exist?(root_file) ? File.read(root_file).strip : nil
61
64
  end
62
65
 
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
66
+ def find_script(relative_path)
67
+ # Try server_root first (gem install or running server's source)
68
+ root = resolve_server_root
69
+ if root
70
+ script = File.join(root, relative_path)
71
+ return script if File.exist?(script)
69
72
  end
73
+ # Fallback: the repo where setup.rb was run from
74
+ fallback = File.join(SETUP_SOURCE_ROOT, relative_path)
75
+ return fallback if File.exist?(fallback)
76
+ nil
70
77
  end
71
- puts({ text: "⚠️", tooltip: "Brainiac server root not found", class: "error" }.to_json)
78
+ RUBY
79
+
80
+ # --- Create wrapper scripts ---
81
+
82
+ # Session slot wrapper (handles all session interactions)
83
+ session_slot_wrapper = File.join(WRAPPER_DIR, "waybar-session-slot")
84
+ File.write(session_slot_wrapper, <<~SCRIPT)
85
+ #!/usr/bin/env ruby
86
+ #{RESOLVER_CODE}
87
+ script = find_script("monitor/waybar/session_slot.rb")
88
+ exec("ruby", script, *ARGV) if script
89
+ puts({ text: "", tooltip: "", class: "" }.to_json) unless ARGV.any? { |a| a.start_with?("--") }
72
90
  SCRIPT
73
- File.chmod(0o755, status_wrapper)
91
+ File.chmod(0o755, session_slot_wrapper)
74
92
 
75
- # Log viewer wrapper
93
+ # Log viewer wrapper (legacy — still available for global manage-all)
76
94
  logs_wrapper = File.join(WRAPPER_DIR, "waybar-logs")
77
95
  File.write(logs_wrapper, <<~SCRIPT)
78
96
  #!/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"
97
+ #{RESOLVER_CODE}
98
+ script = find_script("monitor/waybar/view_logs.rb")
99
+ exec("ruby", script) if script
100
+ warn "Brainiac: view_logs.rb not found"
104
101
  SCRIPT
105
102
  File.chmod(0o755, logs_wrapper)
106
103
 
@@ -108,39 +105,30 @@ File.chmod(0o755, logs_wrapper)
108
105
  deploy_wrapper = File.join(WRAPPER_DIR, "waybar-deploy-env")
109
106
  File.write(deploy_wrapper, <<~SCRIPT)
110
107
  #!/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
108
+ #{RESOLVER_CODE}
109
+ script = find_script("monitor/waybar/deploy_env.rb")
110
+ if script
111
+ load script
112
+ exit
128
113
  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)
114
+ puts({ text: "", tooltip: "Brainiac: deploy_env.rb not found", class: "error" }.to_json)
139
115
  SCRIPT
140
116
  File.chmod(0o755, deploy_wrapper)
141
117
 
142
118
  puts "✓ Created wrapper scripts in #{WRAPPER_DIR}"
143
119
 
120
+ # --- Determine session slot count ---
121
+
122
+ session_slots = MAX_SESSION_SLOTS
123
+ if File.exist?(WAYBAR_JSON)
124
+ waybar_cfg = begin
125
+ JSON.parse(File.read(WAYBAR_JSON))
126
+ rescue StandardError
127
+ {}
128
+ end
129
+ session_slots = waybar_cfg["max_session_slots"] if waybar_cfg["max_session_slots"]
130
+ end
131
+
144
132
  # --- Update waybar config ---
145
133
 
146
134
  config = load_waybar_config
@@ -153,18 +141,26 @@ config = load_waybar_config
153
141
  end
154
142
  config.each_key { |key| config.delete(key) if key.to_s.include?("brainiac") }
155
143
 
156
- # Add agent session module to modules-center
144
+ # Add per-session slot modules to modules-center
157
145
  config["modules-center"] ||= []
158
- config["modules-center"] << "custom/brainiac"
159
146
 
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
- }
147
+ session_slots.times do |i|
148
+ mod_name = "custom/brainiac-session-#{i}"
149
+ config["modules-center"] << mod_name
150
+
151
+ config[mod_name] = {
152
+ "exec" => "#{session_slot_wrapper} #{i}",
153
+ "return-type" => "json",
154
+ "interval" => 3,
155
+ "format" => "{}",
156
+ "tooltip" => true,
157
+ "on-click" => "#{session_slot_wrapper} #{i} --tail",
158
+ "on-click-right" => "#{session_slot_wrapper} #{i} --manage",
159
+ "on-click-middle" => "#{session_slot_wrapper} #{i} --thread"
160
+ }
161
+ end
162
+
163
+ puts "✓ Added #{session_slots} session slot module(s)"
168
164
 
169
165
  # Add per-environment deploy modules (if deployments.json exists)
170
166
  if File.exist?(DEPLOYMENTS_CONFIG)
@@ -172,11 +168,12 @@ if File.exist?(DEPLOYMENTS_CONFIG)
172
168
  envs = (deployments["environments"] || {}).keys
173
169
 
174
170
  center = config["modules-center"]
175
- brainiac_idx = center.index("custom/brainiac") || center.length
171
+ # Insert deploys after the session slots
172
+ insert_idx = center.rindex { |m| m.to_s.include?("brainiac-session") }&.+(1) || center.length
176
173
 
177
174
  envs.each_with_index do |env, i|
178
175
  mod_name = "custom/brainiac-deploy-#{env}"
179
- center.insert(brainiac_idx + i, mod_name)
176
+ center.insert(insert_idx + i, mod_name)
180
177
 
181
178
  config[mod_name] = {
182
179
  "exec" => "#{deploy_wrapper} #{env}",
@@ -200,12 +197,15 @@ puts "✓ Updated waybar config at #{WAYBAR_CONFIG}"
200
197
 
201
198
  style = File.exist?(WAYBAR_STYLE) ? File.read(WAYBAR_STYLE) : ""
202
199
 
203
- unless style.include?("#custom-brainiac")
200
+ unless style.include?("brainiac-session")
201
+ # Remove old single-module style if present
202
+ style = style.gsub(/\/\* Brainiac agent session module \*\/\n#custom-brainiac \{[^}]*\}\n?/, "")
203
+
204
204
  css = <<~CSS
205
205
 
206
- /* Brainiac agent session module */
207
- #custom-brainiac {
208
- padding-right: 100px;
206
+ /* Brainiac per-session slot modules */
207
+ [id^="custom-brainiac-session-"] {
208
+ padding: 0 2px;
209
209
  }
210
210
 
211
211
  /* Brainiac per-environment deploy dots */
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.17
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -146,6 +146,7 @@ files:
146
146
  - monitor/daemon.rb
147
147
  - monitor/shared.rb
148
148
  - monitor/waybar/deploy_env.rb
149
+ - monitor/waybar/session_slot.rb
149
150
  - monitor/waybar/setup.rb
150
151
  - monitor/waybar/status.rb
151
152
  - monitor/waybar/view_logs.rb