brainiac 0.0.17 → 0.0.19
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 +70 -14
- 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: 5f6394c1ba27e48f098ae99072714cfd9cc7ca1c2412b89f62380f5d72e59a7f
|
|
4
|
+
data.tar.gz: ad4cdf5660afddfcadd96456da668452ba5d663f98eafedf2bcbefb3ba328aaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7af17dcf8b530ff9e34bb1338dcbb57006586e0e6fd142ef21268dfb042530db56b5e427a7b7537509c13dfd4fb548674d2509d67c0109e8d3ca240fcca7656e
|
|
7
|
+
data.tar.gz: 4bd7ad87450d7c404f55b48c6e556709c0bd7fd4fc45b3f7f4ed50b97f20553ac704327540d099136a57d6928466cf9fc1750d96ac1bb128a641accc32829378
|
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.19)
|
|
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.19)
|
|
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)")
|
|
@@ -2392,7 +2392,10 @@ when "update", "upgrade"
|
|
|
2392
2392
|
puts "✓ #{gem_name} is already at the latest version (#{current_version})"
|
|
2393
2393
|
end
|
|
2394
2394
|
else
|
|
2395
|
-
# Update brainiac itself: brainiac update
|
|
2395
|
+
# Update brainiac itself + all installed plugins: brainiac update
|
|
2396
|
+
any_updated = false
|
|
2397
|
+
|
|
2398
|
+
# --- Update brainiac core ---
|
|
2396
2399
|
current_version = BRAINIAC_VERSION
|
|
2397
2400
|
|
|
2398
2401
|
puts "Updating brainiac gem..."
|
|
@@ -2404,22 +2407,73 @@ when "update", "upgrade"
|
|
|
2404
2407
|
exit 1
|
|
2405
2408
|
end
|
|
2406
2409
|
|
|
2407
|
-
# Parse installed version from gem install output
|
|
2408
2410
|
new_version = output[/Successfully installed brainiac-(\S+)/, 1]
|
|
2409
2411
|
|
|
2410
2412
|
if new_version
|
|
2411
2413
|
puts "✓ Updated brainiac: #{current_version} → #{new_version}"
|
|
2412
|
-
|
|
2413
|
-
if find_server_pid
|
|
2414
|
-
puts " Restarting server to apply..."
|
|
2415
|
-
wait_for_agents_to_finish
|
|
2416
|
-
stop_server
|
|
2417
|
-
sleep 2
|
|
2418
|
-
start_server(daemon: true)
|
|
2419
|
-
end
|
|
2414
|
+
any_updated = true
|
|
2420
2415
|
else
|
|
2421
2416
|
puts "✓ brainiac is already at the latest version (#{current_version})"
|
|
2422
2417
|
end
|
|
2418
|
+
|
|
2419
|
+
# --- Update all installed plugins ---
|
|
2420
|
+
plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
|
|
2421
|
+
plugins_config = if File.exist?(plugins_file)
|
|
2422
|
+
JSON.parse(File.read(plugins_file))
|
|
2423
|
+
else
|
|
2424
|
+
{ "plugins" => [] }
|
|
2425
|
+
end
|
|
2426
|
+
|
|
2427
|
+
plugins = (plugins_config["plugins"] || []).reject do |p|
|
|
2428
|
+
entry = p.is_a?(Hash) ? p : { "name" => p.to_s }
|
|
2429
|
+
# Skip local-path plugins — they're updated via git
|
|
2430
|
+
entry["path"]
|
|
2431
|
+
end
|
|
2432
|
+
|
|
2433
|
+
if plugins.any?
|
|
2434
|
+
puts ""
|
|
2435
|
+
puts "Checking #{plugins.size} installed plugin(s) for updates..."
|
|
2436
|
+
puts ""
|
|
2437
|
+
|
|
2438
|
+
plugins.each do |p|
|
|
2439
|
+
entry = p.is_a?(Hash) ? p : { "name" => p.to_s }
|
|
2440
|
+
plugin_name = entry["name"]
|
|
2441
|
+
gem_name = entry["gem"] || "brainiac-#{plugin_name}"
|
|
2442
|
+
|
|
2443
|
+
plugin_current = begin
|
|
2444
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
2445
|
+
spec.version.to_s
|
|
2446
|
+
rescue Gem::MissingSpecError
|
|
2447
|
+
nil
|
|
2448
|
+
end
|
|
2449
|
+
|
|
2450
|
+
plugin_output, plugin_status = Open3.capture2e("gem install #{gem_name}")
|
|
2451
|
+
|
|
2452
|
+
unless plugin_status.success?
|
|
2453
|
+
puts " ✗ #{gem_name}: failed to update"
|
|
2454
|
+
next
|
|
2455
|
+
end
|
|
2456
|
+
|
|
2457
|
+
plugin_new = plugin_output[/Successfully installed #{Regexp.escape(gem_name)}-(\S+)/, 1]
|
|
2458
|
+
|
|
2459
|
+
if plugin_new
|
|
2460
|
+
puts " ✓ #{gem_name}: #{plugin_current || "unknown"} → #{plugin_new}"
|
|
2461
|
+
any_updated = true
|
|
2462
|
+
else
|
|
2463
|
+
puts " ✓ #{gem_name}: already at latest (#{plugin_current})"
|
|
2464
|
+
end
|
|
2465
|
+
end
|
|
2466
|
+
end
|
|
2467
|
+
|
|
2468
|
+
# --- Restart server if anything was updated ---
|
|
2469
|
+
if any_updated && find_server_pid
|
|
2470
|
+
puts ""
|
|
2471
|
+
puts "Restarting server to apply updates..."
|
|
2472
|
+
wait_for_agents_to_finish
|
|
2473
|
+
stop_server
|
|
2474
|
+
sleep 2
|
|
2475
|
+
start_server(daemon: true)
|
|
2476
|
+
end
|
|
2423
2477
|
end
|
|
2424
2478
|
|
|
2425
2479
|
when "install"
|
|
@@ -3138,6 +3192,7 @@ when "help", "--help", "-h", nil
|
|
|
3138
3192
|
Usage:
|
|
3139
3193
|
brainiac setup Bootstrap a new machine (deps, dirs, configs)
|
|
3140
3194
|
brainiac server [options] Start the Brainiac webhook server (foreground)
|
|
3195
|
+
brainiac start [options] Alias for 'brainiac server'
|
|
3141
3196
|
brainiac stop Stop the running server
|
|
3142
3197
|
brainiac restart Restart the server
|
|
3143
3198
|
brainiac logs Tail the server log
|
|
@@ -3149,8 +3204,8 @@ when "help", "--help", "-h", nil
|
|
|
3149
3204
|
brainiac projects default <key> Set the default project
|
|
3150
3205
|
brainiac show <key> Show project configuration
|
|
3151
3206
|
brainiac plugin new <name> Generate a new plugin gem skeleton
|
|
3152
|
-
brainiac update Update brainiac
|
|
3153
|
-
brainiac update <plugin> Update
|
|
3207
|
+
brainiac update Update brainiac and all installed plugins
|
|
3208
|
+
brainiac update <plugin> Update a specific installed plugin
|
|
3154
3209
|
brainiac install <plugin> Install a Brainiac plugin (gem-based handler)
|
|
3155
3210
|
brainiac uninstall <plugin> Remove an installed plugin
|
|
3156
3211
|
brainiac plugins List installed plugins
|
|
@@ -3171,7 +3226,7 @@ when "help", "--help", "-h", nil
|
|
|
3171
3226
|
plugin new <name> Generate a new plugin gem skeleton
|
|
3172
3227
|
install <name> Install a plugin (gem install brainiac-<name>)
|
|
3173
3228
|
uninstall <name> Remove a plugin
|
|
3174
|
-
update [name] Update brainiac or a specific plugin
|
|
3229
|
+
update [name] Update brainiac + all plugins, or a specific plugin
|
|
3175
3230
|
plugins List installed plugins
|
|
3176
3231
|
|
|
3177
3232
|
Card Map Commands:
|
|
@@ -3217,6 +3272,7 @@ when "help", "--help", "-h", nil
|
|
|
3217
3272
|
Examples:
|
|
3218
3273
|
# Start the server in the foreground (like rails server)
|
|
3219
3274
|
brainiac server
|
|
3275
|
+
brainiac start
|
|
3220
3276
|
brainiac s
|
|
3221
3277
|
#{" "}
|
|
3222
3278
|
# 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.19
|
|
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
|