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 +4 -4
- data/Gemfile.lock +2 -2
- data/bin/brainiac +3 -1
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/helpers.rb +87 -0
- data/lib/brainiac/prompts.rb +6 -1
- data/lib/brainiac/routes/api.rb +1 -0
- data/lib/brainiac/version.rb +1 -1
- data/monitor/waybar/session_slot.rb +242 -0
- data/monitor/waybar/setup.rb +88 -88
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a5e88ff667021341d3ff4bd4da63212b3918b8c210d276c3c9d0f00ff97e121
|
|
4
|
+
data.tar.gz: ba02b2ab6f6e0e519ef00887da6f53fb6be2dd234fb8db30bc5bc7c9896433b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -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
|
data/lib/brainiac/prompts.rb
CHANGED
|
@@ -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
|
data/lib/brainiac/routes/api.rb
CHANGED
data/lib/brainiac/version.rb
CHANGED
|
@@ -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("&", "&").gsub("<", "<").gsub(">", ">")
|
|
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
|
data/monitor/waybar/setup.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# One-time setup: installs Brainiac modules into waybar config.
|
|
5
5
|
#
|
|
6
6
|
# Adds:
|
|
7
|
-
# -
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
144
|
+
# Add per-session slot modules to modules-center
|
|
157
145
|
config["modules-center"] ||= []
|
|
158
|
-
config["modules-center"] << "custom/brainiac"
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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(
|
|
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?("
|
|
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
|
|
207
|
-
|
|
208
|
-
padding
|
|
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.
|
|
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
|