brainiac 0.1.0 → 0.1.1

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: 82f144b517fd0197684d1912858134c0a6b440263cf2e6bb43d1c4e2651aec3a
4
- data.tar.gz: 415ae3b9fad9b4d407e13147ce0c28286b8292897175f45c37d39b4dfbf2cccd
3
+ metadata.gz: af2bf14f62db5780228f9c8f175abe703dcf0d654c1a35534446117bfced7034
4
+ data.tar.gz: 431b72de572f425911109cc0ced248859456ee7564dd028e5cb502654baf0d7b
5
5
  SHA512:
6
- metadata.gz: 567146d407026c606a08a9c3d592059a43f195f9785318d3763ad368aab002d16153a2b442cf778a06513b231327552201f71b1d7bb2680036214f0cf842e5cc
7
- data.tar.gz: a41c6857915e2e9493f45fa0d49755e4958d6c4de6175a7171d7e25a8013ac3b3f498a7ab1933021c09b0bf3a5768bd1542176d59a0e4ad680e186ce7af74545
6
+ metadata.gz: 87da5739c961603be5884be7dfae1e02c312b8a5649ce612185b19149e0d1ffecb40a97ab0619787ff08c98003d333024565d4e4c7cfa4158631d5db0ea00b00
7
+ data.tar.gz: 70ce18db1d7520f129d286425f07af9a5e58b7f796a462d3af932bf636793db5054a1b2aac90e6b6565189d8b7e83e5f04ee0c7e1d5a56a33b36f9ec5dcde391
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.1.0)
4
+ brainiac (0.1.1)
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.1.0)
87
+ brainiac (0.1.1)
88
88
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
89
89
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
90
90
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Messages from any channel can contain inline tags like:
6
6
  # [project:my-project], [opus], [effort:high], [cli:grok], [chat], [plan],
7
- # [fresh], [branch:feature-xyz], [workitem:wi-abc123]
7
+ # [fresh], [branch:feature-xyz], [workitem:wi-abc123], [profile:k+] ([p:k+])
8
8
  #
9
9
  # This module provides a single parser that extracts all tags and returns
10
10
  # a structured result with the cleaned text.
@@ -16,6 +16,7 @@
16
16
  # model_tag: "opus" or nil (raw tag, not resolved model ID),
17
17
  # effort: "high" or nil,
18
18
  # cli_provider: "grok" or nil,
19
+ # profile: "k+" or nil,
19
20
  # chat_mode: true/false,
20
21
  # planning: true/false,
21
22
  # fresh: true/false,
@@ -29,6 +30,7 @@ def parse_inline_tags(text)
29
30
  model_tag: nil,
30
31
  effort: nil,
31
32
  cli_provider: nil,
33
+ profile: nil,
32
34
  chat_mode: false,
33
35
  planning: false,
34
36
  fresh: false,
@@ -67,6 +69,13 @@ def parse_value_tags(result)
67
69
  result[:clean_text].sub!(match[0], "")
68
70
  end
69
71
 
72
+ # [profile:k+] or short alias [p:k+] — a named env bundle (e.g. an alternate
73
+ # kiro-cli account via XDG_DATA_HOME). Values allow non-word chars like '+'.
74
+ if (match = result[:clean_text].match(/\[(?:profile|p):([^\]]+)\]/i))
75
+ result[:profile] = match[1].strip.downcase
76
+ result[:clean_text].sub!(match[0], "")
77
+ end
78
+
70
79
  # [deploy] or [deploy:dev01]
71
80
  if (match = result[:clean_text].match(/\[deploy(?::([^\]]+))?\]/i))
72
81
  result[:deploy_intent] = match[1]&.strip&.downcase || :auto
@@ -774,7 +774,7 @@ end
774
774
  # Plugins should NOT build their own resume logic; pass `resume: true` and let core handle it.
775
775
  def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
776
776
  source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
777
- message: nil, channel: nil, context: nil, env: {})
777
+ message: nil, channel: nil, context: nil, env: {}, profile: nil)
778
778
  # Intent gate: if a raw message is provided, check whether the agent should respond.
779
779
  return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel, context: context)
780
780
 
@@ -805,7 +805,10 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
805
805
  prompt_file: prompt_file, resume: should_resume,
806
806
  output_file: output_file, chdir: chdir, title: work_item_id,
807
807
  new_session_id: minted_session_id)
808
- spawn_env = agent_env_for(agent_name).merge(env)
808
+ # Profile env (named bundle, e.g. an alternate kiro-cli account) layers between
809
+ # agent env and the explicit `env:` passed in. An explicitly-requested profile
810
+ # wins over agent env; the default profile is only a fallback. See profiles.rb.
811
+ spawn_env = profile_spawn_env(agent_env_for(agent_name), profile).merge(env)
809
812
 
810
813
  log_agent_launch(resolved: resolved, chdir: chdir, log_file: log_file, prompt_file: prompt_file,
811
814
  output_file: output_file, cmd: cmd, should_resume: should_resume,
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Profiles: named bundles of environment variables that can be injected into an
4
+ # agent dispatch without standing up a separate agent.
5
+ #
6
+ # The canonical use case is kiro-cli multi-account: each AWS account gets its own
7
+ # XDG_DATA_HOME (separate auth store), and a profile lets you flip which account a
8
+ # dispatch runs against via an inline tag ([profile:X] / [p:X]) — same CLI, same
9
+ # agent, different credentials.
10
+ #
11
+ # Config lives at ~/.brainiac/profiles.json:
12
+ #
13
+ # {
14
+ # "q": { "env": { "XDG_DATA_HOME": "/home/andy/.local/share" } },
15
+ # "k+": { "env": { "XDG_DATA_HOME": "/home/andy/.brainiac/kiro-accounts/kiro-pro" }, "default": true }
16
+ # }
17
+ #
18
+ # A profile may also optionally pin a model and/or cli_provider:
19
+ #
20
+ # "k+": { "env": {...}, "model": "opus", "cli_provider": "kiro-pro" }
21
+ #
22
+ # Exactly one profile may be marked "default": true — it applies when no [profile:X]
23
+ # tag is present. Because it's only a fallback, an agent's own env (from agents.json)
24
+ # takes precedence over the default profile. An *explicitly requested* profile, by
25
+ # contrast, takes precedence over agent env — that's the whole point of asking for it.
26
+ #
27
+ # Precedence at the spawn point (highest wins):
28
+ # explicit env passed to run_agent
29
+ # > explicitly-requested profile env
30
+ # > agent env (agents.json)
31
+ # > default profile env
32
+ # > DEFAULT_AGENT_ENV
33
+
34
+ PROFILES_FILE = File.join(BRAINIAC_DIR, "profiles.json")
35
+
36
+ def load_profiles_config
37
+ return {} unless File.exist?(PROFILES_FILE)
38
+
39
+ raw = JSON.parse(File.read(PROFILES_FILE))
40
+ LOG.info "Loaded #{raw.size} profile(s) from #{PROFILES_FILE}" if defined?(LOG)
41
+
42
+ # Normalize keys to lowercase for case-insensitive lookup, but preserve the
43
+ # profile body as-is. Profile keys can contain '+' etc. (e.g. "k+").
44
+ normalized = {}
45
+ raw.each { |key, entry| normalized[key.to_s.downcase] = entry }
46
+ normalized
47
+ rescue JSON::ParserError => e
48
+ LOG.error "Failed to parse profiles.json: #{e.message}" if defined?(LOG)
49
+ {}
50
+ end
51
+
52
+ PROFILES = load_profiles_config
53
+
54
+ def reload_profiles!(force: false)
55
+ return unless file_changed?(PROFILES_FILE, force: force)
56
+
57
+ PROFILES.replace(load_profiles_config)
58
+ LOG.info "Reloaded profiles: #{PROFILES.keys.join(", ")}" if defined?(LOG)
59
+ end
60
+
61
+ # Look up a profile entry by name (case-insensitive). Returns the raw hash or nil.
62
+ def profile_entry(profile_name)
63
+ return nil unless profile_name
64
+
65
+ PROFILES[profile_name.to_s.downcase]
66
+ end
67
+
68
+ # Find the profile marked "default": true. Returns [name, entry] or nil.
69
+ def default_profile
70
+ PROFILES.find { |_name, entry| entry.is_a?(Hash) && entry["default"] }
71
+ end
72
+
73
+ # Resolve the env hash contributed by a profile.
74
+ #
75
+ # When profile_name is given and matches a profile, returns that profile's env.
76
+ # When profile_name is nil, returns the default profile's env (or {}).
77
+ # When profile_name is given but unknown, returns {} (never silently falls back
78
+ # to the default — a typo shouldn't route a dispatch to the wrong account).
79
+ #
80
+ # The caller decides precedence relative to agent env — see profile_spawn_env.
81
+ def profile_env(profile_name)
82
+ entry = profile_name ? profile_entry(profile_name) : default_profile&.last
83
+ return {} unless entry.is_a?(Hash)
84
+
85
+ (entry["env"] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
86
+ end
87
+
88
+ # Compute the profile contribution to a spawn env, honoring precedence relative
89
+ # to agent env.
90
+ #
91
+ # - An explicitly-requested profile (profile_name present and known) layers ON TOP
92
+ # of agent env — the request wins, so you can flip accounts for one dispatch.
93
+ # - The default profile (no profile_name given) layers UNDER agent env — it's only
94
+ # a fallback for dispatches that didn't ask for anything.
95
+ # - An unknown profile name is ignored (agent env only) and logged, so a typo never
96
+ # silently swaps accounts.
97
+ #
98
+ # Returns a merged env hash ready to be used as the base for further merges
99
+ # (e.g. run_agent's explicit `env:` still wins by merging afterward).
100
+ def profile_spawn_env(agent_env, profile_name)
101
+ if profile_name
102
+ if profile_entry(profile_name)
103
+ # Explicit request: profile wins over agent env.
104
+ agent_env.merge(profile_env(profile_name))
105
+ else
106
+ LOG.warn "Unknown profile '#{profile_name}' — ignoring (using agent env)" if defined?(LOG)
107
+ agent_env
108
+ end
109
+ else
110
+ # No profile requested: default profile is a baseline agent env overrides.
111
+ default_env = profile_env(nil)
112
+ return agent_env if default_env.empty?
113
+
114
+ default_env.merge(agent_env)
115
+ end
116
+ end
@@ -137,9 +137,10 @@ post "/api/reload" do
137
137
  reload_projects!(force: true)
138
138
  reload_agent_registry!(force: true)
139
139
  reload_user_registry!(force: true)
140
+ reload_profiles!(force: true)
140
141
  ReloadHooks.run_all!
141
142
  { status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
142
- users: USER_REGISTRY["users"].size }.to_json
143
+ users: USER_REGISTRY["users"].size, profiles: PROFILES.keys }.to_json
143
144
  end
144
145
 
145
146
  # --- Agents & Roles ---
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
data/lib/brainiac.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "brainiac/hooks"
2
2
  require_relative "brainiac/config"
3
+ require_relative "brainiac/profiles"
3
4
  require_relative "brainiac/users"
4
5
  require_relative "brainiac/agents"
5
6
  require_relative "brainiac/brain"
data/receiver.rb CHANGED
@@ -84,7 +84,7 @@ set :quiet, true
84
84
  set :server_settings, { Silent: true }
85
85
 
86
86
  # Custom logger that filters polling endpoints unless LOG_LEVEL=debug
87
- SILENT_POLL_PATHS = %w[/api/status /api/deployments].freeze
87
+ SILENT_POLL_PATHS = %w[/api/status /api/deployments /api/sessions/history].freeze
88
88
 
89
89
  class SelectiveLogger < Rack::CommonLogger
90
90
  def call(env)
@@ -0,0 +1,13 @@
1
+ {
2
+ "q": {
3
+ "env": {
4
+ "XDG_DATA_HOME": "/home/you/.local/share"
5
+ }
6
+ },
7
+ "k+": {
8
+ "env": {
9
+ "XDG_DATA_HOME": "/home/you/.brainiac/kiro-accounts/kiro-pro"
10
+ },
11
+ "default": true
12
+ }
13
+ }
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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -139,6 +139,7 @@ files:
139
139
  - lib/brainiac/model_parser.rb
140
140
  - lib/brainiac/notifications.rb
141
141
  - lib/brainiac/plugins.rb
142
+ - lib/brainiac/profiles.rb
142
143
  - lib/brainiac/prompts.rb
143
144
  - lib/brainiac/restart.rb
144
145
  - lib/brainiac/routes/api.rb
@@ -170,6 +171,7 @@ files:
170
171
  - templates/cli-providers/opencode.json.example
171
172
  - templates/hooks/pre-commit
172
173
  - templates/plugins.json.example
174
+ - templates/profiles.json.example
173
175
  - templates/roles/code-reviewer.md.example
174
176
  - templates/roles/general-engineer.md.example
175
177
  - templates/roles/test-engineer.md.example