brainiac 0.4.1 → 0.4.3
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/README.md +3 -0
- data/bin/brainiac +308 -22
- data/bin/brainiac-completion.bash +1 -1
- data/docs/waybar-config.md +13 -0
- data/lib/brainiac/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a563d46d55d8eb7534c68583a1518f89dc5f5d2685ea1d9c55f43f7015861ab
|
|
4
|
+
data.tar.gz: c714c9da68680106d183fab643a2c3dc9589ad21d5b96cdd97dd89945dd64c23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d36fbb11a493c2545554f3a6a244060f6bd13e49c9790b3d65c01959cbf271ac43c2fea62b795f98fde60c42a36b2b0f8117e29dd7a9853d583271715d8468c0
|
|
7
|
+
data.tar.gz: 6a4f88dd9b432d306442172fd15e42eece2424c5436ef8b7cb713673323b029e7b12f6842140fb701aae550dc8352f332b3b627f9bb4054a095c49e0eca92d34
|
data/README.md
CHANGED
|
@@ -932,6 +932,9 @@ Configure how agents appear in the status bar via `~/.brainiac/waybar.json`:
|
|
|
932
932
|
}
|
|
933
933
|
```
|
|
934
934
|
|
|
935
|
+
`brainiac agent create` asks for each new agent's icon (or pass `--icon ⚖️`).
|
|
936
|
+
Change one later with `brainiac agent setup <name> --icon <emoji>`.
|
|
937
|
+
|
|
935
938
|
### macOS (xbar)
|
|
936
939
|
|
|
937
940
|
Requires [xbar](https://xbarapp.com) (free, formerly BitBar).
|
data/bin/brainiac
CHANGED
|
@@ -310,6 +310,156 @@ def agent_persona_description(agent_key)
|
|
|
310
310
|
body.empty? ? nil : body
|
|
311
311
|
end
|
|
312
312
|
|
|
313
|
+
# Markdown that already opens with YAML frontmatter is kept as written, so a
|
|
314
|
+
# hand-made style.md or role file isn't wrapped in a second frontmatter block.
|
|
315
|
+
def frontmatter?(text)
|
|
316
|
+
text.to_s.lstrip.start_with?("---\n")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def available_roles
|
|
320
|
+
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
321
|
+
Dir.exist?(roles_dir) ? Dir.glob(File.join(roles_dir, "*.md")).map { |f| File.basename(f, ".md") }.sort : []
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def role_name_arg(value)
|
|
325
|
+
value.to_s.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Starting point for a new role, shaped like the built-in ones. HTML comment
|
|
329
|
+
# lines are instructions and get stripped by edit_description_in_editor.
|
|
330
|
+
def role_template(role_name, agent_name = nil)
|
|
331
|
+
title = role_name.split("-").map(&:capitalize).join(" ")
|
|
332
|
+
you = agent_name ? "You are #{agent_name}, the #{title.downcase}." : "You are the #{title.downcase}."
|
|
333
|
+
<<~MD
|
|
334
|
+
<!-- Role "#{role_name}": WHAT this agent does. HOW it talks belongs in its persona (style.md). -->
|
|
335
|
+
<!-- Fill in or delete sections. Lines like this one are removed. Save and quit when done; empty = cancel. -->
|
|
336
|
+
# Role: #{title}
|
|
337
|
+
|
|
338
|
+
#{you} Your primary responsibility is ...
|
|
339
|
+
|
|
340
|
+
## Primary Responsibilities
|
|
341
|
+
|
|
342
|
+
- **...**: ...
|
|
343
|
+
|
|
344
|
+
## Checklist
|
|
345
|
+
|
|
346
|
+
When taking on any task:
|
|
347
|
+
|
|
348
|
+
1. ...
|
|
349
|
+
|
|
350
|
+
## What You Don't Do
|
|
351
|
+
|
|
352
|
+
- ...
|
|
353
|
+
|
|
354
|
+
## Looping In Other Agents (opt-in ONLY)
|
|
355
|
+
|
|
356
|
+
Do NOT tag, @mention, or "flag for" other agents by default. Every cross-agent
|
|
357
|
+
mention triggers an automated dispatch that spends the user's credits.
|
|
358
|
+
Only bring another agent in when the user explicitly asks for it. If you think
|
|
359
|
+
another agent should weigh in, say so in plain prose WITHOUT an @mention.
|
|
360
|
+
MD
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Write ~/.brainiac/roles/<name>.md, adding frontmatter unless the text has its own.
|
|
364
|
+
def write_role_file(role_name, text, agent_name = nil)
|
|
365
|
+
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
366
|
+
FileUtils.mkdir_p(roles_dir)
|
|
367
|
+
file = File.join(roles_dir, "#{role_name}.md")
|
|
368
|
+
body = text.strip
|
|
369
|
+
unless frontmatter?(body)
|
|
370
|
+
description = agent_name ? "Role definition for #{agent_name}" : "Role definition: #{role_name}"
|
|
371
|
+
body = "---\nname: #{role_name}\ndescription: #{description}\ninclusion: manual\n---\n\n#{body}"
|
|
372
|
+
end
|
|
373
|
+
File.write(file, "#{body}\n")
|
|
374
|
+
file
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Add roles to a registry entry, keeping the string-or-array shape.
|
|
378
|
+
def assign_roles(entry, new_roles)
|
|
379
|
+
roles = (Array(entry["role"]) + new_roles).uniq
|
|
380
|
+
entry["role"] = roles.size == 1 ? roles.first : roles if roles.any?
|
|
381
|
+
roles
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Interactive role step for create/setup: pick existing roles by number or
|
|
385
|
+
# name, write a new one in $EDITOR, or skip. Returns the chosen role names.
|
|
386
|
+
def prompt_for_agent_role(agent_key, display_name)
|
|
387
|
+
existing = available_roles
|
|
388
|
+
puts ""
|
|
389
|
+
puts "Role for #{display_name} (what they do; the persona is how they talk):"
|
|
390
|
+
existing.each_with_index { |r, i| puts " #{i + 1}. #{r}" }
|
|
391
|
+
puts " n. New role (opens #{ENV["VISUAL"] || ENV["EDITOR"] || "vi"})"
|
|
392
|
+
print "Choose #{"numbers/names (comma-separated), " if existing.any?}n, or blank to skip: "
|
|
393
|
+
input = $stdin.gets.to_s.strip
|
|
394
|
+
return [] if input.empty?
|
|
395
|
+
|
|
396
|
+
if input.downcase.start_with?("n") && !existing.include?(input)
|
|
397
|
+
print "Role name [#{agent_key}]: "
|
|
398
|
+
name = role_name_arg($stdin.gets.to_s.strip)
|
|
399
|
+
name = agent_key if name.empty?
|
|
400
|
+
if existing.include?(name)
|
|
401
|
+
puts " Role '#{name}' already exists; assigning it."
|
|
402
|
+
return [name]
|
|
403
|
+
end
|
|
404
|
+
text = edit_description_in_editor(name, role_template(name, display_name))
|
|
405
|
+
unless text
|
|
406
|
+
puts " Empty role, skipped."
|
|
407
|
+
return []
|
|
408
|
+
end
|
|
409
|
+
puts " Created #{write_role_file(name, text, display_name)}"
|
|
410
|
+
return [name]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
picked = input.split(",").map(&:strip).filter_map do |token|
|
|
414
|
+
token.match?(/\A\d+\z/) ? existing[token.to_i - 1] : token
|
|
415
|
+
end
|
|
416
|
+
unknown = picked - existing
|
|
417
|
+
puts " Unknown role(s) ignored: #{unknown.join(", ")}" if unknown.any?
|
|
418
|
+
picked & existing
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Status bar icons (Omarchy bar, waybar, xbar) live in ~/.brainiac/waybar.json,
|
|
422
|
+
# keyed by display name (matched case-insensitively). Unlisted agents show ❓.
|
|
423
|
+
def bar_config_file
|
|
424
|
+
File.join(BRAINIAC_DIR, "waybar.json")
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def read_bar_config
|
|
428
|
+
File.exist?(bar_config_file) ? JSON.parse(File.read(bar_config_file, encoding: "utf-8")) : {}
|
|
429
|
+
rescue JSON::ParserError
|
|
430
|
+
{}
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def bar_icon(display_name)
|
|
434
|
+
(read_bar_config["agents"] || []).find { |a| a["name"].to_s.casecmp?(display_name) }&.dig("emoji")
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Add or update the agent's bar icon, keeping its color and the other agents.
|
|
438
|
+
# A nil icon removes the agent from the file.
|
|
439
|
+
def write_bar_icon(display_name, icon)
|
|
440
|
+
config = read_bar_config
|
|
441
|
+
agents = config["agents"] || []
|
|
442
|
+
existing = agents.find { |a| a["name"].to_s.casecmp?(display_name) }
|
|
443
|
+
if icon.nil?
|
|
444
|
+
agents.delete(existing)
|
|
445
|
+
elsif existing
|
|
446
|
+
existing["emoji"] = icon
|
|
447
|
+
else
|
|
448
|
+
agents << { "name" => display_name, "emoji" => icon }
|
|
449
|
+
end
|
|
450
|
+
config["agents"] = agents
|
|
451
|
+
config["default_emoji"] ||= "❓"
|
|
452
|
+
config["schema_version"] ||= "1.0"
|
|
453
|
+
config["last_updated"] = Time.now.iso8601
|
|
454
|
+
File.write(bar_config_file, "#{JSON.pretty_generate(config)}\n")
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def prompt_for_bar_icon(display_name)
|
|
458
|
+
print "Status bar icon for #{display_name} (an emoji, blank to skip): "
|
|
459
|
+
icon = $stdin.gets.to_s.strip
|
|
460
|
+
icon.empty? ? nil : icon
|
|
461
|
+
end
|
|
462
|
+
|
|
313
463
|
# Payload handed to plugins' on_agent_created: the registry entry plus the
|
|
314
464
|
# setup inputs plugins need to provision accounts (avatar, description, email).
|
|
315
465
|
# "open_browser" tells plugins whether they may launch a browser for their
|
|
@@ -2435,18 +2585,23 @@ when "agent"
|
|
|
2435
2585
|
puts ""
|
|
2436
2586
|
puts "Options:"
|
|
2437
2587
|
puts " --avatar <path> Profile picture (png/jpg/gif/webp), used by plugins (Discord, Fizzy)"
|
|
2588
|
+
puts " --icon <emoji> Status bar icon (Omarchy bar, waybar, xbar), e.g. --icon ⚖️"
|
|
2438
2589
|
puts " --description <text> Persona description (\"-\" reads stdin, \"@file\" reads a file)"
|
|
2439
2590
|
puts " --description-file <path> Read the persona description from a file"
|
|
2440
2591
|
puts " --email <address> Email for the agent's accounts (default: agent_email_template)"
|
|
2441
2592
|
puts " --local Mark agent as local (dispatches on this machine)"
|
|
2442
2593
|
puts " --role <role> Assign a role (repeatable)"
|
|
2594
|
+
puts " --role-file <path> Create a role from a markdown file and assign it"
|
|
2595
|
+
puts " (named after the agent, or after a new --role name)"
|
|
2443
2596
|
puts " --cli <provider> Set CLI provider (default: kiro)"
|
|
2444
2597
|
puts " --skip <plugins> Don't set up these plugins for this agent, e.g. --skip basecamp"
|
|
2445
2598
|
puts " --only <plugins> Set up just these plugins, e.g. --only discord,fizzy"
|
|
2446
2599
|
puts " --no-browser Print setup links instead of opening them"
|
|
2447
2600
|
puts ""
|
|
2448
2601
|
puts "With no flags and a terminal, interactive mode is used. If no description is"
|
|
2449
|
-
puts "given on a terminal, $EDITOR opens so you can write a full persona."
|
|
2602
|
+
puts "given on a terminal, $EDITOR opens so you can write a full persona. Without"
|
|
2603
|
+
puts "--role/--role-file you're asked to pick an existing role or write a new one,"
|
|
2604
|
+
puts "and without --icon you're asked for a status bar emoji."
|
|
2450
2605
|
puts "After the brainiac files are written, each installed plugin sets up its part"
|
|
2451
2606
|
puts "(Discord bot, Fizzy user, Basecamp person), asking first unless --skip/--only"
|
|
2452
2607
|
puts "is given. Re-run with: brainiac agent setup <name>"
|
|
@@ -2456,6 +2611,8 @@ when "agent"
|
|
|
2456
2611
|
puts " brainiac agent create Quill --local --avatar quill.png --description @quill.md"
|
|
2457
2612
|
puts " brainiac agent create MyAgent # interactive"
|
|
2458
2613
|
puts " brainiac agent create SecurityBot --local --role code-reviewer"
|
|
2614
|
+
puts " brainiac agent create \"Judge Dredd\" --local --avatar dredd.png --icon ⚖️ \\"
|
|
2615
|
+
puts " --description-file dredd-style.md --role-file dredd-role.md"
|
|
2459
2616
|
exit 1
|
|
2460
2617
|
end
|
|
2461
2618
|
|
|
@@ -2470,9 +2627,11 @@ when "agent"
|
|
|
2470
2627
|
|
|
2471
2628
|
local = false
|
|
2472
2629
|
roles = []
|
|
2630
|
+
role_file = nil
|
|
2473
2631
|
cli_provider = nil
|
|
2474
2632
|
persona_text = nil
|
|
2475
2633
|
avatar = nil
|
|
2634
|
+
icon = nil
|
|
2476
2635
|
email = nil
|
|
2477
2636
|
skip_plugins = []
|
|
2478
2637
|
only_plugins = nil
|
|
@@ -2487,7 +2646,10 @@ when "agent"
|
|
|
2487
2646
|
local = true
|
|
2488
2647
|
when "--role"
|
|
2489
2648
|
ARGV.shift
|
|
2490
|
-
roles << ARGV.shift
|
|
2649
|
+
roles << role_name_arg(ARGV.shift)
|
|
2650
|
+
when "--role-file"
|
|
2651
|
+
ARGV.shift
|
|
2652
|
+
role_file = File.expand_path(ARGV.shift.to_s)
|
|
2491
2653
|
when "--cli"
|
|
2492
2654
|
ARGV.shift
|
|
2493
2655
|
cli_provider = ARGV.shift
|
|
@@ -2500,6 +2662,9 @@ when "agent"
|
|
|
2500
2662
|
when "--avatar"
|
|
2501
2663
|
ARGV.shift
|
|
2502
2664
|
avatar = ARGV.shift
|
|
2665
|
+
when "--icon"
|
|
2666
|
+
ARGV.shift
|
|
2667
|
+
icon = ARGV.shift.to_s.strip
|
|
2503
2668
|
when "--email"
|
|
2504
2669
|
ARGV.shift
|
|
2505
2670
|
email = ARGV.shift
|
|
@@ -2533,14 +2698,6 @@ when "agent"
|
|
|
2533
2698
|
input = $stdin.gets.chomp
|
|
2534
2699
|
cli_provider = input.empty? ? nil : input
|
|
2535
2700
|
|
|
2536
|
-
# Show available roles
|
|
2537
|
-
roles_dir = File.join(BRAINIAC_DIR, "roles")
|
|
2538
|
-
available_roles = Dir.exist?(roles_dir) ? Dir.glob(File.join(roles_dir, "*.md")).map { |f| File.basename(f, ".md") } : []
|
|
2539
|
-
hint = available_roles.any? ? " (#{available_roles.join(", ")})" : ""
|
|
2540
|
-
print "Roles#{hint} (comma-separated) []: "
|
|
2541
|
-
input = $stdin.gets.chomp
|
|
2542
|
-
roles = input.empty? ? [] : input.split(",").map(&:strip)
|
|
2543
|
-
|
|
2544
2701
|
print "Avatar image path (or blank to skip) []: "
|
|
2545
2702
|
input = $stdin.gets.chomp
|
|
2546
2703
|
avatar = input.empty? ? nil : input
|
|
@@ -2557,6 +2714,31 @@ when "agent"
|
|
|
2557
2714
|
persona_text = edit_description_in_editor(name) unless $stdin.gets.to_s.strip.downcase.start_with?("n")
|
|
2558
2715
|
end
|
|
2559
2716
|
|
|
2717
|
+
new_role = nil
|
|
2718
|
+
if role_file && !File.file?(role_file)
|
|
2719
|
+
puts "❌ Role file not found: #{role_file}"
|
|
2720
|
+
exit 1
|
|
2721
|
+
end
|
|
2722
|
+
if role_file
|
|
2723
|
+
# --role-file names its role after a --role that doesn't exist yet, else the agent key.
|
|
2724
|
+
new_role = (roles - available_roles).first || agent_key
|
|
2725
|
+
if available_roles.include?(new_role)
|
|
2726
|
+
puts "❌ Role '#{new_role}' already exists. Pass --role <new-name> to name the role from #{role_file}."
|
|
2727
|
+
exit 1
|
|
2728
|
+
end
|
|
2729
|
+
roles |= [new_role]
|
|
2730
|
+
elsif roles.empty? && $stdin.tty?
|
|
2731
|
+
roles = prompt_for_agent_role(agent_key, name)
|
|
2732
|
+
end
|
|
2733
|
+
icon = prompt_for_bar_icon(name) if icon.nil? && $stdin.tty?
|
|
2734
|
+
|
|
2735
|
+
missing_roles = roles - available_roles - [new_role]
|
|
2736
|
+
if missing_roles.any?
|
|
2737
|
+
puts "❌ Unknown role(s): #{missing_roles.join(", ")}. Available: #{available_roles.join(", ")}"
|
|
2738
|
+
puts " Create one with --role-file, or 'brainiac role create <name>'."
|
|
2739
|
+
exit 1
|
|
2740
|
+
end
|
|
2741
|
+
|
|
2560
2742
|
email ||= default_agent_email(agent_key, name)
|
|
2561
2743
|
|
|
2562
2744
|
entry = { "display_name" => name }
|
|
@@ -2575,6 +2757,8 @@ when "agent"
|
|
|
2575
2757
|
end
|
|
2576
2758
|
end
|
|
2577
2759
|
|
|
2760
|
+
puts "✓ Created role #{write_role_file(new_role, File.read(role_file), name)}" if new_role
|
|
2761
|
+
|
|
2578
2762
|
registry[agent_key] = entry
|
|
2579
2763
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2580
2764
|
puts "✓ Created agent '#{name}' (#{agent_key})"
|
|
@@ -2583,13 +2767,22 @@ when "agent"
|
|
|
2583
2767
|
puts " CLI: #{cli_provider}" if cli_provider
|
|
2584
2768
|
puts " Email: #{email}" if email
|
|
2585
2769
|
puts " Avatar: #{entry["avatar"]}" if entry["avatar"]
|
|
2770
|
+
if icon && !icon.empty?
|
|
2771
|
+
write_bar_icon(name, icon)
|
|
2772
|
+
puts " Bar icon: #{icon}"
|
|
2773
|
+
end
|
|
2586
2774
|
|
|
2587
2775
|
# Create persona directory and style file if requested
|
|
2588
2776
|
if persona_text
|
|
2589
2777
|
persona_dir = File.join(BRAINIAC_DIR, "brain", "persona", agent_key)
|
|
2590
2778
|
FileUtils.mkdir_p(persona_dir)
|
|
2591
2779
|
persona_file = File.join(persona_dir, "style.md")
|
|
2592
|
-
|
|
2780
|
+
if File.exist?(persona_file)
|
|
2781
|
+
# Already there (e.g. a re-create); leave it alone.
|
|
2782
|
+
elsif frontmatter?(persona_text)
|
|
2783
|
+
File.write(persona_file, "#{persona_text.strip}\n")
|
|
2784
|
+
puts " Persona: #{persona_file}"
|
|
2785
|
+
else
|
|
2593
2786
|
File.write(persona_file, <<~MD)
|
|
2594
2787
|
---
|
|
2595
2788
|
name: #{agent_key}-style
|
|
@@ -2632,12 +2825,18 @@ when "agent"
|
|
|
2632
2825
|
when "setup"
|
|
2633
2826
|
name = ARGV.shift
|
|
2634
2827
|
unless name
|
|
2635
|
-
puts "Usage: brainiac agent setup <name> [--avatar <path>] [--
|
|
2828
|
+
puts "Usage: brainiac agent setup <name> [--avatar <path>] [--icon <emoji>] [--email <address>] [--role <role>]"
|
|
2829
|
+
puts " [--role-file <path>] [--skip <plugins>] [--only <plugins>] [--no-browser]"
|
|
2636
2830
|
puts ""
|
|
2637
2831
|
puts "Re-runs every plugin's agent setup (Discord bot, Fizzy user, Basecamp person)."
|
|
2638
2832
|
puts "Steps that are already done are skipped, so it's safe to run after an"
|
|
2639
2833
|
puts "interrupted 'agent create' or to set up a newly installed plugin."
|
|
2834
|
+
puts "An agent without a role is asked to pick or write one, and one without a"
|
|
2835
|
+
puts "status bar icon is asked for an emoji."
|
|
2640
2836
|
puts ""
|
|
2837
|
+
puts " --icon e Set the status bar icon (Omarchy bar, waybar, xbar)"
|
|
2838
|
+
puts " --role r Assign an existing role"
|
|
2839
|
+
puts " --role-file f Create a role from a markdown file (named after the agent) and assign it"
|
|
2641
2840
|
puts " --skip a,b Never set up these plugins for this agent (saved)"
|
|
2642
2841
|
puts " --only a,b Run just these plugins (also un-skips them)"
|
|
2643
2842
|
puts " --no-browser Print links instead of opening them"
|
|
@@ -2654,8 +2853,21 @@ when "agent"
|
|
|
2654
2853
|
|
|
2655
2854
|
only_plugins = nil
|
|
2656
2855
|
open_browser = true
|
|
2856
|
+
new_roles = []
|
|
2857
|
+
role_file = nil
|
|
2858
|
+
icon = nil
|
|
2657
2859
|
while ARGV.any?
|
|
2658
2860
|
case ARGV.shift
|
|
2861
|
+
when "--icon"
|
|
2862
|
+
icon = ARGV.shift.to_s.strip
|
|
2863
|
+
when "--role"
|
|
2864
|
+
new_roles << role_name_arg(ARGV.shift)
|
|
2865
|
+
when "--role-file"
|
|
2866
|
+
role_file = File.expand_path(ARGV.shift.to_s)
|
|
2867
|
+
unless File.file?(role_file)
|
|
2868
|
+
puts "❌ Role file not found: #{role_file}"
|
|
2869
|
+
exit 1
|
|
2870
|
+
end
|
|
2659
2871
|
when "--skip"
|
|
2660
2872
|
entry["skip_plugins"] = ((entry["skip_plugins"] || []) + plugin_list_arg(ARGV.shift)).uniq
|
|
2661
2873
|
when "--only"
|
|
@@ -2681,6 +2893,32 @@ when "agent"
|
|
|
2681
2893
|
# Explicitly running a plugin with --only un-skips it for good.
|
|
2682
2894
|
entry["skip_plugins"] -= only_plugins if only_plugins && entry["skip_plugins"]
|
|
2683
2895
|
entry.delete("skip_plugins") if entry["skip_plugins"] == []
|
|
2896
|
+
|
|
2897
|
+
display = entry["display_name"] || name
|
|
2898
|
+
if role_file
|
|
2899
|
+
role_name = (new_roles - available_roles).first || agent_key
|
|
2900
|
+
if available_roles.include?(role_name)
|
|
2901
|
+
puts "❌ Role '#{role_name}' already exists. Pass --role <new-name> to name the role from #{role_file}."
|
|
2902
|
+
exit 1
|
|
2903
|
+
end
|
|
2904
|
+
puts "✓ Created role #{write_role_file(role_name, File.read(role_file), display)}"
|
|
2905
|
+
new_roles |= [role_name]
|
|
2906
|
+
elsif new_roles.empty? && entry["role"].nil? && $stdin.tty?
|
|
2907
|
+
new_roles = prompt_for_agent_role(agent_key, display)
|
|
2908
|
+
end
|
|
2909
|
+
unknown = new_roles - available_roles
|
|
2910
|
+
if unknown.any?
|
|
2911
|
+
puts "❌ Unknown role(s): #{unknown.join(", ")}. Available: #{available_roles.join(", ")}"
|
|
2912
|
+
exit 1
|
|
2913
|
+
end
|
|
2914
|
+
puts "✓ Role: #{assign_roles(entry, new_roles).join(", ")}" if new_roles.any?
|
|
2915
|
+
|
|
2916
|
+
icon = prompt_for_bar_icon(display) if icon.nil? && bar_icon(display).nil? && $stdin.tty?
|
|
2917
|
+
if icon && !icon.empty?
|
|
2918
|
+
write_bar_icon(display, icon)
|
|
2919
|
+
puts "✓ Bar icon: #{icon}"
|
|
2920
|
+
end
|
|
2921
|
+
|
|
2684
2922
|
registry[agent_key] = entry
|
|
2685
2923
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2686
2924
|
|
|
@@ -2709,6 +2947,7 @@ when "agent"
|
|
|
2709
2947
|
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
2710
2948
|
puts "✓ Removed agent '#{display}' (#{agent_key}) from registry"
|
|
2711
2949
|
puts " Note: Persona files at ~/.brainiac/brain/persona/#{agent_key}/ were not deleted."
|
|
2950
|
+
write_bar_icon(display, nil) if bar_icon(display)
|
|
2712
2951
|
|
|
2713
2952
|
# Let plugins clean up their config
|
|
2714
2953
|
notify_plugins_agent_removed(agent_key, display)
|
|
@@ -2951,8 +3190,9 @@ when "role"
|
|
|
2951
3190
|
|
|
2952
3191
|
files.each do |f|
|
|
2953
3192
|
name = File.basename(f, ".md")
|
|
2954
|
-
#
|
|
2955
|
-
|
|
3193
|
+
# First non-empty, non-heading line after any frontmatter is the description
|
|
3194
|
+
body = File.read(f).sub(/\A---\n.*?\n---\n/m, "")
|
|
3195
|
+
desc = body.lines.find { |l| !l.strip.empty? && !l.start_with?("#") }&.strip || ""
|
|
2956
3196
|
agents = agent_roles[name]
|
|
2957
3197
|
agent_str = agents ? " (#{agents.join(", ")})" : ""
|
|
2958
3198
|
puts "#{name}#{agent_str} — #{desc[0..70]}"
|
|
@@ -3049,19 +3289,64 @@ when "role"
|
|
|
3049
3289
|
puts "✓ Removed role '#{role_name}' from #{agent_name}"
|
|
3050
3290
|
|
|
3051
3291
|
when "create", "add"
|
|
3052
|
-
name = ARGV
|
|
3053
|
-
|
|
3054
|
-
puts "Usage: brainiac role create <name>"
|
|
3292
|
+
name = ARGV.shift
|
|
3293
|
+
if name.nil? || name.start_with?("--")
|
|
3294
|
+
puts "Usage: brainiac role create <name> [--from <file>] [--assign <agent>]"
|
|
3295
|
+
puts ""
|
|
3296
|
+
puts "Without --from, $EDITOR opens on a role template (or the template is written"
|
|
3297
|
+
puts "as-is when there's no terminal)."
|
|
3055
3298
|
exit 1
|
|
3056
3299
|
end
|
|
3057
|
-
|
|
3300
|
+
name = role_name_arg(name)
|
|
3301
|
+
from = nil
|
|
3302
|
+
assign_to = nil
|
|
3303
|
+
while ARGV.any?
|
|
3304
|
+
case ARGV.shift
|
|
3305
|
+
when "--from" then from = File.expand_path(ARGV.shift.to_s)
|
|
3306
|
+
when "--assign" then assign_to = ARGV.shift
|
|
3307
|
+
else
|
|
3308
|
+
puts "Unknown option. Run 'brainiac role create' for usage."
|
|
3309
|
+
exit 1
|
|
3310
|
+
end
|
|
3311
|
+
end
|
|
3312
|
+
|
|
3058
3313
|
file = File.join(roles_dir, "#{name}.md")
|
|
3059
3314
|
if File.exist?(file)
|
|
3060
3315
|
puts "Role '#{name}' already exists. Edit #{file} directly."
|
|
3061
3316
|
exit 1
|
|
3062
3317
|
end
|
|
3063
|
-
|
|
3064
|
-
|
|
3318
|
+
if from && !File.file?(from)
|
|
3319
|
+
puts "File not found: #{from}"
|
|
3320
|
+
exit 1
|
|
3321
|
+
end
|
|
3322
|
+
|
|
3323
|
+
agent_registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
3324
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
3325
|
+
agent_key = assign_to && role_name_arg(assign_to)
|
|
3326
|
+
if agent_key && !registry[agent_key]
|
|
3327
|
+
puts "Agent '#{assign_to}' not found."
|
|
3328
|
+
exit 1
|
|
3329
|
+
end
|
|
3330
|
+
agent_display = agent_key && (registry[agent_key]["display_name"] || assign_to)
|
|
3331
|
+
|
|
3332
|
+
text = if from
|
|
3333
|
+
File.read(from)
|
|
3334
|
+
elsif $stdin.tty?
|
|
3335
|
+
edit_description_in_editor(name, role_template(name, agent_display))
|
|
3336
|
+
else
|
|
3337
|
+
role_template(name, agent_display).gsub(/^\s*<!--.*?-->\s*\n?/, "")
|
|
3338
|
+
end
|
|
3339
|
+
unless text
|
|
3340
|
+
puts "Empty role, nothing created."
|
|
3341
|
+
exit 1
|
|
3342
|
+
end
|
|
3343
|
+
puts "✓ Created #{write_role_file(name, text, agent_display)}"
|
|
3344
|
+
|
|
3345
|
+
if agent_key
|
|
3346
|
+
assign_roles(registry[agent_key], [name])
|
|
3347
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
3348
|
+
puts "✓ Assigned role '#{name}' to #{agent_display}"
|
|
3349
|
+
end
|
|
3065
3350
|
|
|
3066
3351
|
else
|
|
3067
3352
|
puts <<~HELP
|
|
@@ -3070,7 +3355,8 @@ when "role"
|
|
|
3070
3355
|
Commands:
|
|
3071
3356
|
list List configured roles
|
|
3072
3357
|
show <name> Show role definition
|
|
3073
|
-
create <name>
|
|
3358
|
+
create <name> [--from <file>] [--assign <agent>]
|
|
3359
|
+
Create a role (opens $EDITOR on a template)
|
|
3074
3360
|
assign <agent> <role> Assign a role to an agent (additive)
|
|
3075
3361
|
unassign <agent> <role> Remove a role from an agent
|
|
3076
3362
|
HELP
|
|
@@ -4392,7 +4678,7 @@ when "help", "--help", "-h", nil
|
|
|
4392
4678
|
Role Commands:
|
|
4393
4679
|
role list List configured roles and assignments
|
|
4394
4680
|
role show <name> Show role definition
|
|
4395
|
-
role create <name>
|
|
4681
|
+
role create <name> [--from f] [--assign a] Create a role (template in $EDITOR)
|
|
4396
4682
|
role assign <agent> <role> Assign a role to an agent (additive)
|
|
4397
4683
|
role unassign <agent> <role> Remove a role from an agent
|
|
4398
4684
|
|
|
@@ -116,7 +116,7 @@ _brainiac() {
|
|
|
116
116
|
case "$agent_name" in
|
|
117
117
|
list|remove|delete|rm|setup) ;;
|
|
118
118
|
create|add)
|
|
119
|
-
COMPREPLY=($(compgen -W "--avatar --description --description-file --email --local --role --cli --skip --only --no-browser" -- "$cur"))
|
|
119
|
+
COMPREPLY=($(compgen -W "--avatar --description --description-file --email --local --role --role-file --cli --skip --only --no-browser" -- "$cur"))
|
|
120
120
|
;;
|
|
121
121
|
*)
|
|
122
122
|
COMPREPLY=($(compgen -W "show env" -- "$cur"))
|
data/docs/waybar-config.md
CHANGED
|
@@ -30,6 +30,19 @@ This file configures how agents appear in the status bar — waybar on Linux, xb
|
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Setting an agent's icon
|
|
34
|
+
|
|
35
|
+
You don't need to edit this file by hand. `brainiac agent create` asks for an
|
|
36
|
+
icon (or takes `--icon`), and `brainiac agent setup` asks for one if the agent
|
|
37
|
+
has none. To change one later:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
brainiac agent setup "Judge Dredd" --icon ⚖️
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The Omarchy bar plugin, waybar and xbar all read this file, so one entry covers
|
|
44
|
+
every bar. Agents without an entry show `default_emoji`.
|
|
45
|
+
|
|
33
46
|
## Fields
|
|
34
47
|
|
|
35
48
|
- **name**: Agent name (must match agent registry)
|
data/lib/brainiac/version.rb
CHANGED