brainiac 0.0.23 → 0.0.25
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 +394 -5
- data/bin/brainiac-completion.bash +28 -4
- data/lib/brainiac/config.rb +1 -1
- data/lib/brainiac/cron.rb +4 -1
- data/lib/brainiac/handlers/shared/git.rb +34 -0
- data/lib/brainiac/helpers.rb +7 -3
- data/lib/brainiac/hooks.rb +2 -0
- data/lib/brainiac/version.rb +1 -1
- data/monitor/shared.rb +11 -1
- data/monitor/xbar/deploy_env.rb +11 -1
- data/templates/cli-providers/kiro.json.example +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: 0a843b412b04f2b176f13e248bcea6327eb27c2893ed516a566c9f79e3b9dbc4
|
|
4
|
+
data.tar.gz: 0bc50a0db4b6e8d5ea67dbf3de47d8481e94a64d886a378730d4d27da5185403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3aa491acebfe3b08aef05735fb593c6835b3757b2502f0ca47ad765aca21d9357a0fbb52c8d0a8f0103ad5aff1ff855d2159ff056297dfa4d7ecfeeb2f4387a
|
|
7
|
+
data.tar.gz: 54179e3366fb8246ed4221e82e70edc67ac0a24fa05212011624f3b9997f11cbdcc481c49b0141efa4c3c8cfbcbb23ffd9f20286442f2a0fb96557ccc8308c12
|
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.25)
|
|
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.25)
|
|
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
|
@@ -527,6 +527,84 @@ def generate_plugin_skill_md(plugin_name, pascal_name)
|
|
|
527
527
|
SKILL
|
|
528
528
|
end
|
|
529
529
|
|
|
530
|
+
SCREENSHOT_CREDS_FILE = File.join(BRAINIAC_DIR, "tmp", "screenshot-creds.json")
|
|
531
|
+
|
|
532
|
+
# Resolve all op:// references in deployments.json and write to a secure cache file.
|
|
533
|
+
# This allows screenshot commands to work without re-authenticating with 1Password.
|
|
534
|
+
def resolve_screenshot_credentials(quiet: false)
|
|
535
|
+
deployments_file = File.join(BRAINIAC_DIR, "deployments.json")
|
|
536
|
+
unless File.exist?(deployments_file)
|
|
537
|
+
puts " ⚠ No deployments.json found, skipping credential resolution." unless quiet
|
|
538
|
+
return false
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
deployments = JSON.parse(File.read(deployments_file))
|
|
542
|
+
environments = deployments["environments"] || {}
|
|
543
|
+
|
|
544
|
+
# Collect all unique op:// references
|
|
545
|
+
op_refs = {}
|
|
546
|
+
environments.each_value do |env_config|
|
|
547
|
+
screenshot = env_config["screenshot"]
|
|
548
|
+
next unless screenshot
|
|
549
|
+
|
|
550
|
+
(screenshot["customers"] || {}).each_value { |ref| op_refs[ref] = nil }
|
|
551
|
+
(screenshot["employees"] || {}).each_value { |ref| op_refs[ref] = nil }
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
if op_refs.empty?
|
|
555
|
+
puts " ⚠ No screenshot credentials to resolve." unless quiet
|
|
556
|
+
return false
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
puts " Resolving #{op_refs.size} credential(s) from 1Password..." unless quiet
|
|
560
|
+
|
|
561
|
+
# Resolve each unique op:// reference
|
|
562
|
+
failed = []
|
|
563
|
+
op_refs.each_key do |ref|
|
|
564
|
+
password, status = Open3.capture2("op", "read", ref)
|
|
565
|
+
if status.success?
|
|
566
|
+
op_refs[ref] = password.chomp
|
|
567
|
+
else
|
|
568
|
+
failed << ref
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
if failed.any?
|
|
573
|
+
puts " ❌ Failed to resolve #{failed.size} credential(s):" unless quiet
|
|
574
|
+
failed.each { |ref| puts " #{ref}" } unless quiet
|
|
575
|
+
return false
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Write resolved credentials to cache file with restrictive permissions
|
|
579
|
+
cache_dir = File.join(BRAINIAC_DIR, "tmp")
|
|
580
|
+
FileUtils.mkdir_p(cache_dir)
|
|
581
|
+
File.write(SCREENSHOT_CREDS_FILE, JSON.pretty_generate(op_refs))
|
|
582
|
+
File.chmod(0o600, SCREENSHOT_CREDS_FILE)
|
|
583
|
+
|
|
584
|
+
puts " ✓ Screenshot credentials cached (#{op_refs.size} resolved)" unless quiet
|
|
585
|
+
true
|
|
586
|
+
rescue JSON::ParserError => e
|
|
587
|
+
puts " ❌ Failed to parse deployments.json: #{e.message}" unless quiet
|
|
588
|
+
false
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Read a resolved credential from the cache file, falling back to `op read`.
|
|
592
|
+
def read_screenshot_credential(op_ref)
|
|
593
|
+
# Try cache first
|
|
594
|
+
if File.exist?(SCREENSHOT_CREDS_FILE)
|
|
595
|
+
creds = JSON.parse(File.read(SCREENSHOT_CREDS_FILE))
|
|
596
|
+
return creds[op_ref] if creds[op_ref]
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Fallback: resolve directly via op read
|
|
600
|
+
password, status = Open3.capture2("op", "read", op_ref)
|
|
601
|
+
return password.chomp if status.success?
|
|
602
|
+
|
|
603
|
+
nil
|
|
604
|
+
rescue JSON::ParserError
|
|
605
|
+
nil
|
|
606
|
+
end
|
|
607
|
+
|
|
530
608
|
def start_server(daemon: false)
|
|
531
609
|
# Resolve the real path of the brainiac script (follows symlinks)
|
|
532
610
|
receiver_path = File.join(BRAINIAC_ROOT, "receiver.rb")
|
|
@@ -544,6 +622,9 @@ def start_server(daemon: false)
|
|
|
544
622
|
exit 1
|
|
545
623
|
end
|
|
546
624
|
|
|
625
|
+
# Pre-resolve screenshot credentials from 1Password (so agents can screenshot without re-auth)
|
|
626
|
+
resolve_screenshot_credentials
|
|
627
|
+
|
|
547
628
|
if daemon
|
|
548
629
|
# Daemon mode: start in background (like the old behavior)
|
|
549
630
|
log_dir = File.join(receiver_dir, "tmp")
|
|
@@ -1107,8 +1188,7 @@ when "unregister", "remove", "rm"
|
|
|
1107
1188
|
when "list", "ls"
|
|
1108
1189
|
list_projects
|
|
1109
1190
|
|
|
1110
|
-
when "projects"
|
|
1111
|
-
# Support "brainiac projects list" as an alias for "brainiac list"
|
|
1191
|
+
when "projects", "project"
|
|
1112
1192
|
projects_cmd = ARGV.shift
|
|
1113
1193
|
case projects_cmd
|
|
1114
1194
|
when "list", "ls", nil
|
|
@@ -1140,9 +1220,140 @@ when "projects"
|
|
|
1140
1220
|
save_projects(projects)
|
|
1141
1221
|
|
|
1142
1222
|
puts "✓ Set '#{project_key}' as the default project"
|
|
1223
|
+
when "update", "set"
|
|
1224
|
+
project_key = ARGV.shift
|
|
1225
|
+
field = ARGV.shift
|
|
1226
|
+
value = ARGV.join(" ") # Allow multi-word values (e.g. tags with spaces)
|
|
1227
|
+
|
|
1228
|
+
unless project_key && field
|
|
1229
|
+
puts "Usage: brainiac projects update <key> <field> <value>"
|
|
1230
|
+
puts ""
|
|
1231
|
+
puts "Fields:"
|
|
1232
|
+
puts " repo_path <path> Path to the project repository"
|
|
1233
|
+
puts " github_repo <owner/repo> GitHub repository (e.g. stowzilla/brainiac)"
|
|
1234
|
+
puts " fizzy_tags <tag1,tag2> Comma-separated Fizzy tags"
|
|
1235
|
+
puts " fizzy_board <board> Default Fizzy board key"
|
|
1236
|
+
puts " cli_provider <provider> CLI provider name (e.g. kiro, grok)"
|
|
1237
|
+
puts " agent_name <name> Default agent for this project"
|
|
1238
|
+
puts " agent_model <model> Default model (e.g. auto, opus, sonnet)"
|
|
1239
|
+
puts " default_branch <branch> Default git branch (e.g. main, master)"
|
|
1240
|
+
puts ""
|
|
1241
|
+
puts "Examples:"
|
|
1242
|
+
puts " brainiac projects update brainiac fizzy_board stowzilla"
|
|
1243
|
+
puts " brainiac projects update marketplace cli_provider grok"
|
|
1244
|
+
puts " brainiac projects update brainiac fizzy_tags brainiac,core"
|
|
1245
|
+
puts " brainiac projects update marketplace agent_model opus"
|
|
1246
|
+
exit 1
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
projects = load_projects
|
|
1250
|
+
unless projects.key?(project_key)
|
|
1251
|
+
puts "Error: Project '#{project_key}' not found."
|
|
1252
|
+
puts "Available projects: #{projects.keys.join(", ")}"
|
|
1253
|
+
exit 1
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
# Handle special field parsing
|
|
1257
|
+
case field
|
|
1258
|
+
when "fizzy_tags", "tags"
|
|
1259
|
+
field = "fizzy_tags"
|
|
1260
|
+
unless value && !value.empty?
|
|
1261
|
+
puts "Error: Value required. Provide comma-separated tags."
|
|
1262
|
+
exit 1
|
|
1263
|
+
end
|
|
1264
|
+
parsed_value = value.split(",").map(&:strip)
|
|
1265
|
+
when "repo_path"
|
|
1266
|
+
unless value && !value.empty?
|
|
1267
|
+
puts "Error: Value required."
|
|
1268
|
+
exit 1
|
|
1269
|
+
end
|
|
1270
|
+
parsed_value = File.expand_path(value)
|
|
1271
|
+
puts "Warning: Path does not exist: #{parsed_value}" unless Dir.exist?(parsed_value)
|
|
1272
|
+
else
|
|
1273
|
+
# Handles known fields (github_repo, fizzy_board, cli_provider, agent_name, agent_model,
|
|
1274
|
+
# default_branch) and arbitrary fields for forward-compatibility
|
|
1275
|
+
unless value && !value.empty?
|
|
1276
|
+
puts "Error: Value required."
|
|
1277
|
+
exit 1
|
|
1278
|
+
end
|
|
1279
|
+
parsed_value = value
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
projects[project_key][field] = parsed_value
|
|
1283
|
+
save_projects(projects)
|
|
1284
|
+
|
|
1285
|
+
display_value = parsed_value.is_a?(Array) ? parsed_value.join(", ") : parsed_value
|
|
1286
|
+
puts "✓ Updated #{project_key}: #{field} = #{display_value}"
|
|
1287
|
+
|
|
1288
|
+
when "unset", "delete-field"
|
|
1289
|
+
project_key = ARGV.shift
|
|
1290
|
+
field = ARGV.shift
|
|
1291
|
+
|
|
1292
|
+
unless project_key && field
|
|
1293
|
+
puts "Usage: brainiac projects unset <key> <field>"
|
|
1294
|
+
puts ""
|
|
1295
|
+
puts "Removes a field from a project's configuration."
|
|
1296
|
+
puts ""
|
|
1297
|
+
puts "Examples:"
|
|
1298
|
+
puts " brainiac projects unset marketplace fizzy_board"
|
|
1299
|
+
puts " brainiac projects unset brainiac default_branch"
|
|
1300
|
+
exit 1
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
projects = load_projects
|
|
1304
|
+
unless projects.key?(project_key)
|
|
1305
|
+
puts "Error: Project '#{project_key}' not found."
|
|
1306
|
+
exit 1
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1309
|
+
unless projects[project_key].key?(field)
|
|
1310
|
+
puts "Error: Field '#{field}' not set on project '#{project_key}'."
|
|
1311
|
+
exit 1
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
projects[project_key].delete(field)
|
|
1315
|
+
save_projects(projects)
|
|
1316
|
+
puts "✓ Removed #{field} from #{project_key}"
|
|
1317
|
+
|
|
1318
|
+
when "help", "--help", "-h"
|
|
1319
|
+
puts <<~HELP
|
|
1320
|
+
Usage: brainiac projects <command> [options]
|
|
1321
|
+
|
|
1322
|
+
Commands:
|
|
1323
|
+
list List all registered projects (default)
|
|
1324
|
+
show <key> Show detailed configuration for a project
|
|
1325
|
+
default <key> Set the default project (fallback when no tags match)
|
|
1326
|
+
update <key> <field> <value> Update a project config field
|
|
1327
|
+
unset <key> <field> Remove a field from project config
|
|
1328
|
+
|
|
1329
|
+
Aliases:
|
|
1330
|
+
brainiac project Works the same as 'brainiac projects'
|
|
1331
|
+
brainiac list Shortcut for 'brainiac projects list'
|
|
1332
|
+
brainiac show Shortcut for 'brainiac projects show'
|
|
1333
|
+
|
|
1334
|
+
Update fields:
|
|
1335
|
+
repo_path Path to the project repository
|
|
1336
|
+
github_repo GitHub repository (owner/repo)
|
|
1337
|
+
fizzy_tags Comma-separated Fizzy tags
|
|
1338
|
+
fizzy_board Default Fizzy board key
|
|
1339
|
+
cli_provider CLI provider name (kiro, grok, etc.)
|
|
1340
|
+
agent_name Default agent for this project
|
|
1341
|
+
agent_model Default model (auto, opus, sonnet, haiku, etc.)
|
|
1342
|
+
default_branch Default git branch
|
|
1343
|
+
|
|
1344
|
+
Examples:
|
|
1345
|
+
brainiac projects list
|
|
1346
|
+
brainiac project show marketplace
|
|
1347
|
+
brainiac projects default marketplace
|
|
1348
|
+
brainiac projects update brainiac fizzy_board stowzilla
|
|
1349
|
+
brainiac projects update marketplace agent_model opus
|
|
1350
|
+
brainiac projects update brainiac fizzy_tags brainiac,core
|
|
1351
|
+
brainiac projects unset marketplace fizzy_board
|
|
1352
|
+
HELP
|
|
1143
1353
|
else
|
|
1144
1354
|
puts "Unknown projects command: #{projects_cmd}"
|
|
1145
|
-
puts "Available: list, show, default"
|
|
1355
|
+
puts "Available: list, show, default, update, unset, help"
|
|
1356
|
+
puts "Run 'brainiac projects help' for more details."
|
|
1146
1357
|
exit 1
|
|
1147
1358
|
end
|
|
1148
1359
|
|
|
@@ -3173,6 +3384,183 @@ when "plugin"
|
|
|
3173
3384
|
puts " brainiac plugins List installed plugins"
|
|
3174
3385
|
end
|
|
3175
3386
|
|
|
3387
|
+
when "screenshot"
|
|
3388
|
+
# Resolve screenshot credentials from cached file and delegate to project's screenshot script.
|
|
3389
|
+
# Usage: brainiac screenshot [app-name] [page-path] [--env ENV] [extra args...]
|
|
3390
|
+
#
|
|
3391
|
+
# Credentials are resolved from 1Password at server start and cached in
|
|
3392
|
+
# ~/.brainiac/tmp/screenshot-creds.json. Falls back to `op read` if cache is missing.
|
|
3393
|
+
# Run `brainiac screenshot resolve` to manually refresh the cache.
|
|
3394
|
+
|
|
3395
|
+
deployments_file = File.join(BRAINIAC_DIR, "deployments.json")
|
|
3396
|
+
abort "Error: #{deployments_file} not found. Run 'brainiac setup' or create it manually." unless File.exist?(deployments_file)
|
|
3397
|
+
|
|
3398
|
+
deployments = JSON.parse(File.read(deployments_file))
|
|
3399
|
+
environments = deployments["environments"] || {}
|
|
3400
|
+
|
|
3401
|
+
sub = ARGV.shift
|
|
3402
|
+
case sub
|
|
3403
|
+
when "envs", "list"
|
|
3404
|
+
environments.each do |key, env|
|
|
3405
|
+
screenshot = env["screenshot"]
|
|
3406
|
+
if screenshot
|
|
3407
|
+
customers = (screenshot["customers"] || {}).keys
|
|
3408
|
+
employees = (screenshot["employees"] || {}).keys
|
|
3409
|
+
puts " #{key} (#{env["label"]})"
|
|
3410
|
+
puts " Customers: #{customers.join(", ")}" if customers.any?
|
|
3411
|
+
puts " Employees: #{employees.join(", ")}" if employees.any?
|
|
3412
|
+
else
|
|
3413
|
+
puts " #{key} (#{env["label"]}) — no screenshot config"
|
|
3414
|
+
end
|
|
3415
|
+
end
|
|
3416
|
+
|
|
3417
|
+
when "resolve"
|
|
3418
|
+
puts "Resolving screenshot credentials from 1Password..."
|
|
3419
|
+
if resolve_screenshot_credentials(quiet: false)
|
|
3420
|
+
puts "\nCredentials cached at #{SCREENSHOT_CREDS_FILE}"
|
|
3421
|
+
puts "Screenshots will use the cache — no 1Password re-auth needed."
|
|
3422
|
+
else
|
|
3423
|
+
abort "\nFailed to resolve credentials. Ensure `op` is signed in."
|
|
3424
|
+
end
|
|
3425
|
+
|
|
3426
|
+
when "help", "--help", "-h", nil
|
|
3427
|
+
puts <<~HELP
|
|
3428
|
+
brainiac screenshot — Take screenshots with credentials from deployments.json
|
|
3429
|
+
|
|
3430
|
+
Usage:
|
|
3431
|
+
brainiac screenshot <app-name> <page-path> [options]
|
|
3432
|
+
|
|
3433
|
+
Credentials are resolved from 1Password at server start and cached in
|
|
3434
|
+
#{SCREENSHOT_CREDS_FILE} (mode 0600). Use `brainiac screenshot resolve`
|
|
3435
|
+
to manually refresh the cache.
|
|
3436
|
+
|
|
3437
|
+
Options:
|
|
3438
|
+
--env ENV Environment to use (default: auto-detect from project owner)
|
|
3439
|
+
--customer EMAIL Use specific customer email (default: first in list)
|
|
3440
|
+
--employee EMAIL Use specific employee email (default: first in list)
|
|
3441
|
+
--project KEY Project key (default: current directory or default project)
|
|
3442
|
+
All other args are passed through to screenshot-page.sh
|
|
3443
|
+
|
|
3444
|
+
Commands:
|
|
3445
|
+
brainiac screenshot resolve Resolve and cache credentials from 1Password
|
|
3446
|
+
brainiac screenshot envs List environments with screenshot config
|
|
3447
|
+
brainiac screenshot help Show this help
|
|
3448
|
+
|
|
3449
|
+
Examples:
|
|
3450
|
+
brainiac screenshot ops-app /api-explorer
|
|
3451
|
+
brainiac screenshot ops-app /inventory --env dev04
|
|
3452
|
+
brainiac screenshot app /dashboard --env dev02
|
|
3453
|
+
HELP
|
|
3454
|
+
|
|
3455
|
+
else
|
|
3456
|
+
# Parse flags from remaining args
|
|
3457
|
+
app_name = sub
|
|
3458
|
+
page_path = ARGV.shift
|
|
3459
|
+
abort "Error: page path required. Usage: brainiac screenshot <app-name> <page-path> [options]" unless page_path
|
|
3460
|
+
|
|
3461
|
+
env_name = nil
|
|
3462
|
+
customer_email = nil
|
|
3463
|
+
employee_email = nil
|
|
3464
|
+
project_key = nil
|
|
3465
|
+
passthrough_args = []
|
|
3466
|
+
|
|
3467
|
+
while (arg = ARGV.shift)
|
|
3468
|
+
case arg
|
|
3469
|
+
when "--env"
|
|
3470
|
+
env_name = ARGV.shift
|
|
3471
|
+
when "--customer"
|
|
3472
|
+
customer_email = ARGV.shift
|
|
3473
|
+
when "--employee"
|
|
3474
|
+
employee_email = ARGV.shift
|
|
3475
|
+
when "--project"
|
|
3476
|
+
project_key = ARGV.shift
|
|
3477
|
+
else
|
|
3478
|
+
passthrough_args << arg
|
|
3479
|
+
# If the flag takes a value, pass it through too
|
|
3480
|
+
passthrough_args << ARGV.shift if arg.start_with?("--") && !arg.include?("=") && ARGV.first && !ARGV.first.start_with?("--")
|
|
3481
|
+
end
|
|
3482
|
+
end
|
|
3483
|
+
|
|
3484
|
+
# Determine environment (default: dev02 for Linux)
|
|
3485
|
+
env_name ||= case RUBY_PLATFORM
|
|
3486
|
+
when /darwin/ then "dev01"
|
|
3487
|
+
else "dev02"
|
|
3488
|
+
end
|
|
3489
|
+
|
|
3490
|
+
env_config = environments[env_name]
|
|
3491
|
+
abort "Error: Environment '#{env_name}' not found in deployments.json. Available: #{environments.keys.join(", ")}" unless env_config
|
|
3492
|
+
|
|
3493
|
+
screenshot_config = env_config["screenshot"]
|
|
3494
|
+
abort "Error: No 'screenshot' config for environment '#{env_name}' in deployments.json." unless screenshot_config
|
|
3495
|
+
|
|
3496
|
+
customers = screenshot_config["customers"] || {}
|
|
3497
|
+
employees = screenshot_config["employees"] || {}
|
|
3498
|
+
|
|
3499
|
+
# Resolve which email to use
|
|
3500
|
+
if app_name == "ops-app"
|
|
3501
|
+
email = employee_email || employees.keys.first
|
|
3502
|
+
op_ref = employees[email]
|
|
3503
|
+
abort "Error: Employee email '#{email}' not found in #{env_name} screenshot config. Available: #{employees.keys.join(", ")}" unless op_ref
|
|
3504
|
+
else
|
|
3505
|
+
email = customer_email || customers.keys.first
|
|
3506
|
+
op_ref = customers[email]
|
|
3507
|
+
abort "Error: Customer email '#{email}' not found in #{env_name} screenshot config. Available: #{customers.keys.join(", ")}" unless op_ref
|
|
3508
|
+
end
|
|
3509
|
+
|
|
3510
|
+
# Resolve password (from cache or 1Password directly)
|
|
3511
|
+
password = read_screenshot_credential(op_ref)
|
|
3512
|
+
unless password
|
|
3513
|
+
abort "Error: Failed to resolve password for #{op_ref}. Run 'brainiac screenshot resolve' or 'brainiac restart' to refresh the cache."
|
|
3514
|
+
end
|
|
3515
|
+
|
|
3516
|
+
# Determine project path
|
|
3517
|
+
projects = load_projects
|
|
3518
|
+
if project_key
|
|
3519
|
+
project = projects[project_key]
|
|
3520
|
+
abort "Error: Project '#{project_key}' not found." unless project
|
|
3521
|
+
else
|
|
3522
|
+
# Try to find project from current directory or env config
|
|
3523
|
+
project = projects.find { |_k, p| p["repo_path"] == Dir.pwd }&.last
|
|
3524
|
+
project ||= projects[env_config["project"]]
|
|
3525
|
+
end
|
|
3526
|
+
|
|
3527
|
+
abort "Error: Could not determine project. Use --project KEY." unless project
|
|
3528
|
+
repo_path = project["repo_path"]
|
|
3529
|
+
script_path = File.join(repo_path, "scripts", "screenshot-page.sh")
|
|
3530
|
+
abort "Error: Screenshot script not found at #{script_path}" unless File.exist?(script_path)
|
|
3531
|
+
|
|
3532
|
+
# Build environment
|
|
3533
|
+
screenshot_env = {
|
|
3534
|
+
"SCREENSHOT_CUSTOMER_EMAIL" => (app_name == "ops-app" ? (customers.keys.first || email) : email),
|
|
3535
|
+
"SCREENSHOT_CUSTOMER_PASSWORD" => password,
|
|
3536
|
+
"SCREENSHOT_OPS_EMAIL" => (app_name == "ops-app" ? email : (employees.keys.first || email)),
|
|
3537
|
+
"SCREENSHOT_OPS_PASSWORD" => password
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
# Also set environment-specific email vars
|
|
3541
|
+
env_upper = env_name.upcase.tr("-", "_")
|
|
3542
|
+
screenshot_env["SCREENSHOT_#{env_upper}_CUSTOMER_EMAIL"] = customers.keys.first if customers.any?
|
|
3543
|
+
|
|
3544
|
+
# Run the screenshot script
|
|
3545
|
+
cmd = [script_path, app_name, page_path, "", ""] + passthrough_args
|
|
3546
|
+
puts "Taking screenshot: #{app_name} #{page_path} (env: #{env_name}, user: #{email})"
|
|
3547
|
+
puts "Running: #{cmd.join(" ")}"
|
|
3548
|
+
|
|
3549
|
+
pid = spawn(screenshot_env, *cmd, chdir: repo_path)
|
|
3550
|
+
Process.wait(pid)
|
|
3551
|
+
|
|
3552
|
+
if $CHILD_STATUS.success?
|
|
3553
|
+
# Find the most recent screenshot
|
|
3554
|
+
attachments_dir = File.join(repo_path, ".fizzy-attachments")
|
|
3555
|
+
if Dir.exist?(attachments_dir)
|
|
3556
|
+
latest = Dir.glob(File.join(attachments_dir, "*.png")).max_by { |f| File.mtime(f) }
|
|
3557
|
+
puts "\n✅ Screenshot saved: #{latest}" if latest
|
|
3558
|
+
end
|
|
3559
|
+
else
|
|
3560
|
+
abort "\n❌ Screenshot failed (exit #{$CHILD_STATUS.exitstatus})"
|
|
3561
|
+
end
|
|
3562
|
+
end
|
|
3563
|
+
|
|
3176
3564
|
when "version", "--version", "-v"
|
|
3177
3565
|
puts "brainiac #{BRAINIAC_VERSION}"
|
|
3178
3566
|
|
|
@@ -3200,8 +3588,8 @@ when "help", "--help", "-h", nil
|
|
|
3200
3588
|
brainiac register [options] Register current directory as a project
|
|
3201
3589
|
brainiac unregister <key> Unregister a project
|
|
3202
3590
|
brainiac list List all registered projects
|
|
3203
|
-
brainiac projects list
|
|
3204
|
-
brainiac
|
|
3591
|
+
brainiac projects <command> Manage projects (list, show, default, help)
|
|
3592
|
+
brainiac project <command> Alias for 'brainiac projects'
|
|
3205
3593
|
brainiac show <key> Show project configuration
|
|
3206
3594
|
brainiac plugin new <name> Generate a new plugin gem skeleton
|
|
3207
3595
|
brainiac update Update brainiac and all installed plugins
|
|
@@ -3214,6 +3602,7 @@ when "help", "--help", "-h", nil
|
|
|
3214
3602
|
brainiac provider <command> Manage CLI providers
|
|
3215
3603
|
brainiac role <command> Manage agent roles
|
|
3216
3604
|
brainiac agent <command> Manage agent registry (env, list, show)
|
|
3605
|
+
brainiac screenshot <app> <path> Take screenshot with 1Password credentials
|
|
3217
3606
|
brainiac config Configure Brainiac CLI
|
|
3218
3607
|
brainiac path Show Brainiac config directory
|
|
3219
3608
|
brainiac version Show version
|
|
@@ -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 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"
|
|
12
|
+
local commands="server start stop restart logs status register unregister list show brain cron provider role agent config path version help setup projects project 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
|
|
@@ -180,16 +180,40 @@ _brainiac() {
|
|
|
180
180
|
esac
|
|
181
181
|
;;
|
|
182
182
|
|
|
183
|
-
projects)
|
|
183
|
+
projects|project)
|
|
184
184
|
case $cword in
|
|
185
185
|
2)
|
|
186
|
-
COMPREPLY=($(compgen -W "list default" -- "$cur"))
|
|
186
|
+
COMPREPLY=($(compgen -W "list show default update unset help" -- "$cur"))
|
|
187
187
|
;;
|
|
188
188
|
3)
|
|
189
|
-
|
|
189
|
+
local subcmd="${words[2]}"
|
|
190
|
+
if [[ "$subcmd" == "default" || "$subcmd" == "show" || "$subcmd" == "update" || "$subcmd" == "unset" ]]; then
|
|
190
191
|
COMPREPLY=($(compgen -W "$(_brainiac_projects)" -- "$cur"))
|
|
191
192
|
fi
|
|
192
193
|
;;
|
|
194
|
+
4)
|
|
195
|
+
local subcmd="${words[2]}"
|
|
196
|
+
if [[ "$subcmd" == "update" || "$subcmd" == "unset" ]]; then
|
|
197
|
+
COMPREPLY=($(compgen -W "repo_path github_repo fizzy_tags fizzy_board cli_provider agent_name agent_model default_branch" -- "$cur"))
|
|
198
|
+
fi
|
|
199
|
+
;;
|
|
200
|
+
5)
|
|
201
|
+
local subcmd="${words[2]}"
|
|
202
|
+
local field="${words[4]}"
|
|
203
|
+
if [[ "$subcmd" == "update" ]]; then
|
|
204
|
+
case "$field" in
|
|
205
|
+
cli_provider)
|
|
206
|
+
COMPREPLY=($(compgen -W "$(_brainiac_providers)" -- "$cur"))
|
|
207
|
+
;;
|
|
208
|
+
agent_model)
|
|
209
|
+
COMPREPLY=($(compgen -W "auto opus sonnet haiku deepseek minimax qwen" -- "$cur"))
|
|
210
|
+
;;
|
|
211
|
+
agent_name)
|
|
212
|
+
COMPREPLY=($(compgen -W "$(_brainiac_agents)" -- "$cur"))
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
fi
|
|
216
|
+
;;
|
|
193
217
|
esac
|
|
194
218
|
;;
|
|
195
219
|
|
data/lib/brainiac/config.rb
CHANGED
|
@@ -157,7 +157,7 @@ DEFAULT_PROJECT = {
|
|
|
157
157
|
"agent_effort_flag" => ENV["AGENT_EFFORT_FLAG"] || "--effort",
|
|
158
158
|
"agent_effort" => ENV.fetch("AGENT_EFFORT", nil),
|
|
159
159
|
"allowed_models" => {
|
|
160
|
-
"opus" => "claude-opus-4.
|
|
160
|
+
"opus" => "claude-opus-4.5",
|
|
161
161
|
"sonnet" => "claude-sonnet-4.6",
|
|
162
162
|
"haiku" => "claude-haiku-4.5",
|
|
163
163
|
"deepseek" => "deepseek-3.2",
|
data/lib/brainiac/cron.rb
CHANGED
|
@@ -466,7 +466,10 @@ def handle_cron_completion(job, project, agent_name, agent_config_name, log_file
|
|
|
466
466
|
update_cron_job_state(job)
|
|
467
467
|
|
|
468
468
|
if File.exist?(response_file)
|
|
469
|
-
|
|
469
|
+
response_content = File.read(response_file).strip
|
|
470
|
+
LOG.info "[Cron] Job #{job[:id]} completed. Response: #{response_content[0..100]}..."
|
|
471
|
+
|
|
472
|
+
notify_cron_output(job, response_content, agent_name: agent_name) if job[:notify_target] && !response_content.empty?
|
|
470
473
|
else
|
|
471
474
|
LOG.warn "[Cron] Job #{job[:id]} produced no response"
|
|
472
475
|
end
|
|
@@ -106,6 +106,40 @@ def run_project_hook(repo_path, hook_name, extra_env: {})
|
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Resolve the base branch for a new worktree.
|
|
110
|
+
# Emits :resolve_base_branch hook — plugins can override the default (origin/main).
|
|
111
|
+
# Returns a ref string (e.g., "origin/main", "epic/my-feature").
|
|
112
|
+
#
|
|
113
|
+
# @param repo_path [String] Path to the git repo
|
|
114
|
+
# @param card_number [Integer, String, nil] Fizzy card number (if applicable)
|
|
115
|
+
# @param project_key [String, nil] Brainiac project key
|
|
116
|
+
# @return [String] Base ref for the worktree
|
|
117
|
+
def resolve_base_branch(repo_path:, card_number: nil, project_key: nil)
|
|
118
|
+
results = Brainiac.emit(:resolve_base_branch,
|
|
119
|
+
repo_path: repo_path, card_number: card_number, project_key: project_key)
|
|
120
|
+
# First non-nil result wins (plugins return a branch name or nil to skip)
|
|
121
|
+
custom = results.compact.first
|
|
122
|
+
if custom
|
|
123
|
+
LOG.info "Using custom base branch '#{custom}' (from plugin hook)"
|
|
124
|
+
custom
|
|
125
|
+
else
|
|
126
|
+
"origin/#{get_default_branch(repo_path)}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Resolve the PR target branch for a card.
|
|
131
|
+
# Emits :resolve_pr_target hook — plugins can override the default branch.
|
|
132
|
+
#
|
|
133
|
+
# @param repo_path [String] Path to the git repo
|
|
134
|
+
# @param card_number [Integer, String, nil] Fizzy card number
|
|
135
|
+
# @param project_key [String, nil] Brainiac project key
|
|
136
|
+
# @return [String, nil] Target branch name (without origin/ prefix), or nil for default
|
|
137
|
+
def resolve_pr_target(repo_path:, card_number: nil, project_key: nil)
|
|
138
|
+
results = Brainiac.emit(:resolve_pr_target,
|
|
139
|
+
repo_path: repo_path, card_number: card_number, project_key: project_key)
|
|
140
|
+
results.compact.first
|
|
141
|
+
end
|
|
142
|
+
|
|
109
143
|
# Create or reuse a git worktree for a given branch.
|
|
110
144
|
# Returns the worktree path on success.
|
|
111
145
|
def create_or_reuse_worktree(repo_path:, branch:, base_ref: nil, worktree_path: nil)
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -492,7 +492,11 @@ end
|
|
|
492
492
|
def intent_skip?(message, agent_name:, source: nil, channel: nil, context: nil)
|
|
493
493
|
return false unless message && agent_name && intent_config["enabled"]
|
|
494
494
|
|
|
495
|
+
# Some channels always require a response (e.g. PR comments are inherently directed at the agent).
|
|
496
|
+
bypass_channels = intent_config["bypass_channels"] || %w[github]
|
|
495
497
|
intent_channel = channel || source&.to_s || "conversation"
|
|
498
|
+
return false if bypass_channels.any? { |bc| intent_channel.to_s.downcase.include?(bc) }
|
|
499
|
+
|
|
496
500
|
unless check_intent(message, agent_name: agent_name, channel: intent_channel, context: context)
|
|
497
501
|
LOG.info "[Intent] Skipping dispatch for #{agent_name} — message classified as not requiring response"
|
|
498
502
|
return true
|
|
@@ -503,7 +507,7 @@ end
|
|
|
503
507
|
|
|
504
508
|
def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
|
|
505
509
|
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
|
|
506
|
-
message: nil, channel: nil, context: nil)
|
|
510
|
+
message: nil, channel: nil, context: nil, env: {})
|
|
507
511
|
# Intent gate: if a raw message is provided, check whether the agent should respond.
|
|
508
512
|
return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel, context: context)
|
|
509
513
|
|
|
@@ -527,7 +531,7 @@ def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil
|
|
|
527
531
|
cmd = build_agent_cmd(resolved, agent_config_name: agent_config_name, model: model, effort: effort, prompt_file: prompt_file, resume: should_resume)
|
|
528
532
|
prompt_mode = resolved["prompt_mode"] || "stdin"
|
|
529
533
|
|
|
530
|
-
spawn_env = agent_env_for(agent_name)
|
|
534
|
+
spawn_env = agent_env_for(agent_name).merge(env)
|
|
531
535
|
|
|
532
536
|
LOG.info "Running #{resolved["agent_cli"]} in #{chdir}, logging to #{log_file}"
|
|
533
537
|
LOG.info "Prompt written to #{prompt_file}"
|
|
@@ -585,7 +589,7 @@ def build_agent_cmd(resolved, agent_config_name: nil, model: nil, effort: nil, p
|
|
|
585
589
|
# "auto" means "let the CLI choose" — skip passing it unless the provider explicitly maps it.
|
|
586
590
|
if model && resolved["agent_model_flag"] && !resolved["agent_model_flag"].empty?
|
|
587
591
|
allowed = resolved["allowed_models"] || {}
|
|
588
|
-
# Pass the model if it's a mapped value (e.g. "claude-opus-4.
|
|
592
|
+
# Pass the model if it's a mapped value (e.g. "claude-opus-4.5") or the key itself is mapped
|
|
589
593
|
is_known = allowed.value?(model) || allowed.key?(model)
|
|
590
594
|
cmd.push(resolved["agent_model_flag"], model) if is_known
|
|
591
595
|
end
|
data/lib/brainiac/hooks.rb
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
# :build_brain_context — When building brain context (plugins add source-specific queries)
|
|
18
18
|
# :pre_dispatch — Before dispatching an agent (plugins can inject config)
|
|
19
19
|
# :post_comment — After an agent posts a comment/response
|
|
20
|
+
# :resolve_base_branch — Resolve custom base branch for worktree creation
|
|
21
|
+
# :resolve_pr_target — Resolve custom PR target branch (instead of default branch)
|
|
20
22
|
#
|
|
21
23
|
# Usage (in plugin .register):
|
|
22
24
|
# Brainiac.on(:agent_completed) do |ctx|
|
data/lib/brainiac/version.rb
CHANGED
data/monitor/shared.rb
CHANGED
|
@@ -205,7 +205,17 @@ def deploy_bash_script(env_key, worktree:, aws_profile: nil)
|
|
|
205
205
|
fi
|
|
206
206
|
rm -f "$logfile"
|
|
207
207
|
echo
|
|
208
|
-
if [ $status -eq 0 ]; then
|
|
208
|
+
if [ $status -eq 0 ]; then
|
|
209
|
+
echo "✅ Deploy complete"
|
|
210
|
+
curl -s -X POST http://localhost:4567/api/deployments/#{env_key.shellescape} \
|
|
211
|
+
-H "Content-Type: application/json" \
|
|
212
|
+
-d '{"worktree": #{worktree.to_json}, "deployed_by": "waybar"}' > /dev/null 2>&1
|
|
213
|
+
else
|
|
214
|
+
echo "❌ Deploy failed (exit $status)"
|
|
215
|
+
curl -s -X POST http://localhost:4567/api/deployments/#{env_key.shellescape}/failed \
|
|
216
|
+
-H "Content-Type: application/json" \
|
|
217
|
+
-d '{}' > /dev/null 2>&1
|
|
218
|
+
fi
|
|
209
219
|
echo "Press Enter to close..."
|
|
210
220
|
read
|
|
211
221
|
BASH
|
data/monitor/xbar/deploy_env.rb
CHANGED
|
@@ -111,7 +111,17 @@ deploy_script = <<~BASH
|
|
|
111
111
|
fi
|
|
112
112
|
rm -f "$logfile"
|
|
113
113
|
echo
|
|
114
|
-
if [ $status -eq 0 ]; then
|
|
114
|
+
if [ $status -eq 0 ]; then
|
|
115
|
+
echo "✅ Deploy complete"
|
|
116
|
+
curl -s -X POST http://localhost:4567/api/deployments/#{Shellwords.escape(env_key)} \
|
|
117
|
+
-H "Content-Type: application/json" \
|
|
118
|
+
-d '{"worktree": #{worktree.to_json}, "deployed_by": "waybar"}' > /dev/null 2>&1
|
|
119
|
+
else
|
|
120
|
+
echo "❌ Deploy failed (exit $status)"
|
|
121
|
+
curl -s -X POST http://localhost:4567/api/deployments/#{Shellwords.escape(env_key)}/failed \
|
|
122
|
+
-H "Content-Type: application/json" \
|
|
123
|
+
-d '{}' > /dev/null 2>&1
|
|
124
|
+
fi
|
|
115
125
|
echo
|
|
116
126
|
echo "Press any key to close..."
|
|
117
127
|
read -n 1
|