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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1edc9fb64e331daacfa390596ce230ee37f6739e91efc72438434110e88d24de
4
- data.tar.gz: 0fdf11e2eace81f2ce56f6f562e675d7c67598232a23b020fcc982f7ac20db58
3
+ metadata.gz: 4a563d46d55d8eb7534c68583a1518f89dc5f5d2685ea1d9c55f43f7015861ab
4
+ data.tar.gz: c714c9da68680106d183fab643a2c3dc9589ad21d5b96cdd97dd89945dd64c23
5
5
  SHA512:
6
- metadata.gz: ca3e17f678f0207947da8fda7af7711788cacdf377e2496b04c82a1aed303faa3cc57e6c3b32be3227a98bc12b86bb28b25d523ab817514b396a249955cc226a
7
- data.tar.gz: 2cab777438aa1b245b037a46f6fdf9dcfe16120eca888528dd045f7e70fed699c850bfe76b858601f9f7708a61c213f3be96cb4173a3f2fae81d83f7f4fcc36b
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
- unless File.exist?(persona_file)
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>] [--email <address>] [--skip <plugins>] [--only <plugins>] [--no-browser]"
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
- # Read first non-empty, non-heading line as description
2955
- desc = File.readlines(f).find { |l| !l.strip.empty? && !l.start_with?("#") && !l.start_with?("---") }&.strip || ""
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[0]
3053
- unless name
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
- FileUtils.mkdir_p(roles_dir)
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
- File.write(file, "# Role: #{name.split("-").map(&:capitalize).join(" ")}\n\nDescribe this role's responsibilities here.\n")
3064
- puts "Created #{file} — edit it to define the role."
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> Create a new role file
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> Create a new role file
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"))
@@ -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)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.4.1"
5
+ VERSION = "0.4.3"
6
6
  end
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.4.1
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis