brainiac 0.0.27 → 0.0.29

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: 889b0c7491371dd4deb7dc5a173eb5c45045cf9d85a9bf1b5cfcf75bb233d407
4
- data.tar.gz: c5532f51f711e5b5839fd3c9cbfe3e297e8ae00849a44b9b82331d13d5d318f0
3
+ metadata.gz: 37f135fffb0c94919fa376fee96e12053af77062f93dadc520cce7285ee86e78
4
+ data.tar.gz: 553b10319590bde4fee8f89cf227059fec83d750d6ad7188d56aa5243a6cf169
5
5
  SHA512:
6
- metadata.gz: 81735abd0d5d4dc9e9aeccaf3ae80e7c51f3ca529f759750e0dc8ad4f3f15a3bee54ea3169f96e9a6e3a7b378d8395940e9b5af10bd92e8117d0fc49a602d855
7
- data.tar.gz: 71bfdf3580bbe77aa873be4d7ef211b42f9f19979efce7a76cdcbfdeb30c914bdf9dae48b32d571bf28ad9871f53c1fd92946f025e36d7d9aa2a4e2b38ad4df5
6
+ metadata.gz: e04d26de1e9010e175b69032358e400223af29ffabde834e48b206d6cff58340fe1082262b36843fe6995aff752df2ed053793a78f416cc47d1dd4b47b881a99
7
+ data.tar.gz: fe32e3768064d1728a2b28d40aef900c1afb454d04e192b67a9e4f631bf117a147ebef547898709ead380b286336dee64da8a40b62cf6f10b7a327ca1bb3d505
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.0.27)
4
+ brainiac (0.0.29)
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.27)
87
+ brainiac (0.0.29)
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/README.md CHANGED
@@ -226,12 +226,23 @@ Configure which AI CLI to use for dispatching agents. Each provider is a JSON fi
226
226
 
227
227
  ```json
228
228
  {
229
- "agent_cli": "kiro-cli",
230
- "agent_cli_args": "chat --trust-all-tools --no-interactive",
231
- "agent_model_flag": "--model"
229
+ "binary": "kiro-cli",
230
+ "default_args": "chat --trust-all-tools --no-interactive",
231
+ "agent_flag": "--agent",
232
+ "model_flag": "--model"
232
233
  }
233
234
  ```
234
235
 
236
+ The `agent_flag` field controls how agent identity is passed to the CLI. Different CLIs use different flags:
237
+
238
+ | CLI | `agent_flag` | Example command |
239
+ |-----|-------------|-----------------|
240
+ | Kiro CLI | `"--agent"` | `kiro-cli --agent sherlock chat --no-interactive` |
241
+ | Codex CLI | `"--profile"` | `codex --profile sherlock exec --sandbox workspace-write -` |
242
+ | Grok CLI | `null` | `grok --always-approve` (no agent identity passed) |
243
+
244
+ Set `agent_flag` to `null` to suppress passing the agent name entirely (for CLIs that don't support persona switching).
245
+
235
246
  Brainiac ships with example providers during setup. Your project config references which provider to use.
236
247
 
237
248
  #### 3. Environment Variables
data/bin/brainiac CHANGED
@@ -205,6 +205,7 @@ def wait_for_agents_to_finish
205
205
  end
206
206
  end
207
207
 
208
+ # rubocop:disable Naming/PredicateMethod
208
209
  def stop_server
209
210
  pid = find_server_pid
210
211
 
@@ -245,6 +246,7 @@ def stop_server
245
246
  false
246
247
  end
247
248
  end
249
+ # rubocop:enable Naming/PredicateMethod
248
250
 
249
251
  # Kill a single process: TERM first, KILL if it doesn't exit within 5s.
250
252
  def kill_pid(pid, quiet: false)
@@ -1025,6 +1027,9 @@ def brain_list
1025
1027
  puts "Run 'brainiac brain init <agent-name>' to set up a new agent."
1026
1028
  end
1027
1029
 
1030
+ # Model output parsing (shared with lib/brainiac/helpers.rb)
1031
+ require_relative "../lib/brainiac/model_parser"
1032
+
1028
1033
  # Main CLI
1029
1034
  options = {}
1030
1035
  subcommand = ARGV.shift
@@ -1940,6 +1945,150 @@ when "provider"
1940
1945
  File.write(file, JSON.pretty_generate(scaffold))
1941
1946
  puts "Created #{file} — edit it to configure the provider."
1942
1947
 
1948
+ when "list-models"
1949
+ name = ARGV[0]
1950
+ unless name
1951
+ puts "Usage: brainiac provider list-models <name>"
1952
+ exit 1
1953
+ end
1954
+ file = File.join(providers_dir, "#{name}.json")
1955
+ unless File.exist?(file)
1956
+ puts "Provider '#{name}' not found."
1957
+ exit 1
1958
+ end
1959
+ data = JSON.parse(File.read(file))
1960
+ command = data["list_models_command"]
1961
+ if command.nil? || command.empty?
1962
+ puts "Provider '#{name}' has no list_models_command configured."
1963
+ puts "Edit #{file} and set \"list_models_command\" to a shell command that lists models."
1964
+ puts "Example: \"kiro-cli chat --list-models --format json\""
1965
+ exit 1
1966
+ end
1967
+
1968
+ puts "Querying models from '#{name}' (#{command})..."
1969
+ puts ""
1970
+ stdout, stderr, status = Open3.capture3(command)
1971
+ unless status.success?
1972
+ puts "Command failed (exit #{status.exitstatus}):"
1973
+ puts stderr.strip unless stderr.empty?
1974
+ exit 1
1975
+ end
1976
+
1977
+ models = parse_list_models_output(stdout)
1978
+ if models.nil? || models.empty?
1979
+ puts "No models found in command output."
1980
+ puts "Raw output:"
1981
+ puts stdout
1982
+ exit 1
1983
+ end
1984
+
1985
+ # Filter hidden models (e.g. codex "visibility": "hide") unless none are visible
1986
+ visible_models = models.select { |m| m["visibility"].nil? || m["visibility"] != "hide" }
1987
+ visible_models = models if visible_models.empty?
1988
+
1989
+ # Display models in a table
1990
+ default_model = data["default_model"]
1991
+ # Determine default: explicit default_model field > parsed "default" flag > first model
1992
+ has_explicit_default = default_model && visible_models.any? { |m| m["model_id"] == default_model }
1993
+ has_parsed_default = !has_explicit_default && visible_models.any? { |m| m["default"] }
1994
+ puts "Available models for '#{name}' (#{visible_models.size} visible, #{models.size} total):"
1995
+ puts ""
1996
+ visible_models.each do |m|
1997
+ model_id = m["model_id"] || "unknown"
1998
+ desc = m["description"] || m["display_name"] || ""
1999
+ rate = m["rate_multiplier"] ? "#{m["rate_multiplier"]}x" : (m["rate"] || "")
2000
+ marker = if has_explicit_default
2001
+ m["model_id"] == default_model ? "*" : " "
2002
+ elsif has_parsed_default
2003
+ m["default"] ? "*" : " "
2004
+ else
2005
+ m == visible_models.first ? "*" : " "
2006
+ end
2007
+ if desc.empty?
2008
+ puts " #{marker} #{model_id}"
2009
+ else
2010
+ puts format(" %<marker>s %-25<model>s %-15<rate>s %<desc>s", marker: marker, model: model_id, rate: rate, desc: desc)
2011
+ end
2012
+ end
2013
+
2014
+ # Show which are mapped vs unmapped
2015
+ configured = data["models"] || {}
2016
+ configured_ids = configured.values
2017
+ unmapped = visible_models.select do |m|
2018
+ id = m["model_id"]
2019
+ !configured_ids.include?(id) && !configured.key?(id)
2020
+ end
2021
+ unless unmapped.empty?
2022
+ puts ""
2023
+ puts "#{unmapped.size} model(s) not in your provider's 'models' map:"
2024
+ unmapped.each { |m| puts " - #{m["model_id"]}" }
2025
+ puts ""
2026
+ puts "Run 'brainiac provider sync-models #{name}' to add them."
2027
+ end
2028
+
2029
+ when "sync-models"
2030
+ name = ARGV[0]
2031
+ unless name
2032
+ puts "Usage: brainiac provider sync-models <name>"
2033
+ exit 1
2034
+ end
2035
+ file = File.join(providers_dir, "#{name}.json")
2036
+ unless File.exist?(file)
2037
+ puts "Provider '#{name}' not found."
2038
+ exit 1
2039
+ end
2040
+ data = JSON.parse(File.read(file))
2041
+ command = data["list_models_command"]
2042
+ if command.nil? || command.empty?
2043
+ puts "Provider '#{name}' has no list_models_command configured."
2044
+ exit 1
2045
+ end
2046
+
2047
+ stdout, stderr, status = Open3.capture3(command)
2048
+ unless status.success?
2049
+ puts "Command failed (exit #{status.exitstatus}): #{stderr.strip}"
2050
+ exit 1
2051
+ end
2052
+
2053
+ models = parse_list_models_output(stdout)
2054
+ if models.nil? || models.empty?
2055
+ puts "No models found in command output."
2056
+ exit 1
2057
+ end
2058
+
2059
+ # Build a models map from discovered models: key = short name, value = model_id
2060
+ existing_models = data["models"] || {}
2061
+ new_models = existing_models.dup
2062
+ added = []
2063
+
2064
+ models.each do |m|
2065
+ model_id = m["model_id"]
2066
+ next unless model_id
2067
+
2068
+ # Skip hidden models (e.g. codex internal models)
2069
+ next if m["visibility"] == "hide"
2070
+
2071
+ # Skip if already mapped (as key or value)
2072
+ next if new_models.value?(model_id) || new_models.key?(model_id)
2073
+
2074
+ # Generate a short key: strip common prefixes, use the base name
2075
+ short_key = generate_short_model_key(model_id)
2076
+ # If the short key already exists, use the full model_id as the key
2077
+ short_key = model_id if new_models.key?(short_key)
2078
+ new_models[short_key] = model_id
2079
+ added << "#{short_key} => #{model_id}"
2080
+ end
2081
+
2082
+ if added.empty?
2083
+ puts "All discovered models are already mapped. Nothing to sync."
2084
+ exit 0
2085
+ end
2086
+
2087
+ data["models"] = new_models
2088
+ File.write(file, JSON.pretty_generate(data))
2089
+ puts "Synced #{added.size} new model(s) to #{file}:"
2090
+ added.each { |a| puts " + #{a}" }
2091
+
1943
2092
  else
1944
2093
  puts <<~HELP
1945
2094
  Usage: brainiac provider <command>
@@ -1948,6 +2097,8 @@ when "provider"
1948
2097
  list List configured CLI providers
1949
2098
  show <name> Show provider configuration
1950
2099
  add <name> Create a new provider config
2100
+ list-models <name> Query available models from the CLI provider
2101
+ sync-models <name> Add discovered models to provider config
1951
2102
  HELP
1952
2103
  end
1953
2104
 
@@ -3474,7 +3625,7 @@ when "plugin"
3474
3625
  gem_name = "brainiac-#{plugin_name}"
3475
3626
 
3476
3627
  # Determine the base repo path (strip worktree suffix like --branch-name)
3477
- base_path = current_path.sub(/--[^\/]+$/, "")
3628
+ base_path = current_path.sub(%r{--[^/]+$}, "")
3478
3629
  unless Dir.exist?(base_path)
3479
3630
  puts "Error: Base repo not found at #{base_path}"
3480
3631
  exit 1
@@ -3503,9 +3654,7 @@ when "plugin"
3503
3654
  # Fetch latest to make sure the branch is available
3504
3655
  puts "Fetching latest from origin..."
3505
3656
  _out, _err, fetch_status = Open3.capture3("git", "fetch", "origin", chdir: base_path)
3506
- unless fetch_status.success?
3507
- puts "Warning: git fetch failed — continuing with local state."
3508
- end
3657
+ puts "Warning: git fetch failed — continuing with local state." unless fetch_status.success?
3509
3658
 
3510
3659
  # Check if a worktree already exists for this branch
3511
3660
  worktree_list, _, wt_status = Open3.capture3("git", "worktree", "list", "--porcelain", chdir: base_path)
@@ -3513,18 +3662,19 @@ when "plugin"
3513
3662
 
3514
3663
  if wt_status.success?
3515
3664
  # Parse porcelain output to find worktrees on the target branch
3516
- worktrees = worktree_list.split("\n\n").map do |block|
3665
+ worktree_blocks = worktree_list.split("\n\n").map do |block|
3517
3666
  wt = {}
3518
3667
  block.each_line do |line|
3519
3668
  case line
3520
3669
  when /^worktree (.+)/
3521
3670
  wt[:path] = Regexp.last_match(1).strip
3522
- when /^branch refs\/heads\/(.+)/
3671
+ when %r{^branch refs/heads/(.+)}
3523
3672
  wt[:branch] = Regexp.last_match(1).strip
3524
3673
  end
3525
3674
  end
3526
3675
  wt
3527
- end.select { |wt| wt[:path] && wt[:branch] }
3676
+ end
3677
+ worktrees = worktree_blocks.select { |wt| wt[:path] && wt[:branch] }
3528
3678
 
3529
3679
  target_worktree = worktrees.find { |wt| wt[:branch] == branch_name }
3530
3680
  end
@@ -3541,11 +3691,9 @@ when "plugin"
3541
3691
  File.write(plugins_file, JSON.pretty_generate(plugins_config))
3542
3692
  puts "✓ Switched plugin '#{plugin_name}' to branch '#{branch_name}'"
3543
3693
  puts " Path: #{new_path}"
3544
- puts " Restart the server to apply: brainiac restart"
3545
3694
  else
3546
3695
  # Check if branch exists remotely or locally
3547
- branch_exists = false
3548
- check_cmd, _, check_status = Open3.capture3("git", "rev-parse", "--verify", "origin/#{branch_name}", chdir: base_path)
3696
+ _, _, check_status = Open3.capture3("git", "rev-parse", "--verify", "origin/#{branch_name}", chdir: base_path)
3549
3697
  branch_exists = check_status.success?
3550
3698
 
3551
3699
  unless branch_exists
@@ -3576,7 +3724,10 @@ when "plugin"
3576
3724
  _, _, local_check = Open3.capture3("git", "rev-parse", "--verify", branch_name, chdir: base_path)
3577
3725
  if !local_check.success? && check_status.success?
3578
3726
  # Remote branch exists but not local — create tracking branch in worktree
3579
- _, stderr, wt_status = Open3.capture3("git", "worktree", "add", "--track", "-b", branch_name, worktree_dir, "origin/#{branch_name}", chdir: base_path)
3727
+ _, stderr, wt_status = Open3.capture3(
3728
+ "git", "worktree", "add", "--track", "-b", branch_name, worktree_dir,
3729
+ "origin/#{branch_name}", chdir: base_path
3730
+ )
3580
3731
  else
3581
3732
  # Local branch exists — use it directly
3582
3733
  _, stderr, wt_status = Open3.capture3("git", "worktree", "add", worktree_dir, branch_name, chdir: base_path)
@@ -3592,8 +3743,8 @@ when "plugin"
3592
3743
  File.write(plugins_file, JSON.pretty_generate(plugins_config))
3593
3744
  puts "✓ Switched plugin '#{plugin_name}' to branch '#{branch_name}'"
3594
3745
  puts " Worktree: #{worktree_dir}"
3595
- puts " Restart the server to apply: brainiac restart"
3596
3746
  end
3747
+ puts " Restart the server to apply: brainiac restart"
3597
3748
 
3598
3749
  else
3599
3750
  puts "Usage: brainiac plugin <command>"
data/docs/resume.md ADDED
@@ -0,0 +1,87 @@
1
+ # Resume Session Integration
2
+
3
+ Brainiac supports resuming prior agent sessions instead of starting fresh. Two patterns exist:
4
+
5
+ ## Flag-Based Resume (e.g. Kiro `--resume`, Grok `-c`)
6
+
7
+ The CLI appends a flag to the existing command. The base command structure stays the same.
8
+
9
+ ```
10
+ kiro-cli --agent sherlock chat --trust-all-tools --no-interactive --resume
11
+ grok --always-approve -c
12
+ ```
13
+
14
+ Provider config:
15
+
16
+ ```json
17
+ {
18
+ "resume_flag": "--resume"
19
+ }
20
+ ```
21
+
22
+ ## Subcommand-Based Resume (e.g. Codex `exec resume --last`)
23
+
24
+ The CLI changes the subcommand entirely. The base command structure is replaced.
25
+
26
+ ```
27
+ codex exec --full-auto → codex exec resume --last --full-auto
28
+ ```
29
+
30
+ Provider config:
31
+
32
+ ```json
33
+ {
34
+ "resume_flag": null,
35
+ "resume_args": "exec resume --last --full-auto",
36
+ "session_dir": "~/.codex/sessions"
37
+ }
38
+ ```
39
+
40
+ When `resume_args` is set, it replaces `default_args` entirely during resume. The `session_dir` field tells Brainiac where to look for prior sessions (for CLIs that store session state centrally rather than in the project directory).
41
+
42
+ ## Plugin Integration Contract
43
+
44
+ Plugins that want to support resume MUST:
45
+
46
+ 1. **Use `resume_viable?`** to check whether a session can be resumed:
47
+
48
+ ```ruby
49
+ can_resume = resume_viable?(project_config: config, chdir: worktree_path)
50
+ ```
51
+
52
+ 2. **Pass `resume: true` to `run_agent`** — core handles the rest:
53
+
54
+ ```ruby
55
+ run_agent(prompt, project_config: config, chdir: worktree_path, resume: can_resume)
56
+ ```
57
+
58
+ Plugins MUST NOT:
59
+ - Check `resolved["resume_flag"]` directly (misses `resume_args` providers)
60
+ - Call `resolve_resume` directly (internal to `run_agent`)
61
+ - Build their own resume command logic
62
+
63
+ ## How It Works Internally
64
+
65
+ ```
66
+ Plugin calls run_agent(..., resume: true)
67
+ → run_agent calls resolve_resume(true, resolved, chdir)
68
+ → resolve_resume checks resume_flag OR resume_args is configured
69
+ → resolve_resume calls prior_session_exists?(chdir, cli, session_dir:)
70
+ → For centralized session dirs: scans .jsonl files for matching cwd
71
+ → For local session dirs: checks for .grok/, .kiro-cli/, or recent logs
72
+ → Returns :resume_args, flag string, or false
73
+ → run_agent passes result to build_agent_cmd
74
+ → :resume_args → replace default_args with resume_args
75
+ → String flag → append to command
76
+ → false → normal command (no resume)
77
+ ```
78
+
79
+ ## Session Detection
80
+
81
+ ### Local (flag-based CLIs)
82
+
83
+ Checks for CLI-specific dotdirs (`.grok/`, `.kiro-cli/`) or recent agent logs in `tmp/` (last 24 hours).
84
+
85
+ ### Centralized (subcommand-based CLIs)
86
+
87
+ Scans `session_dir` for `.jsonl` files modified in the last 24 hours whose first-line JSON metadata contains a `payload.cwd` matching the target working directory. Handles symlink resolution (e.g. `/var/folders` vs `/private/var/folders` on macOS).
data/lib/brainiac/cron.rb CHANGED
@@ -596,10 +596,12 @@ def build_cron_agent_cmd(job, project, prompt_file: nil)
596
596
  resolved = resolve_project_cli_config(project, agent_name: job[:agent])
597
597
  agent_flag = resolved.key?("agent_flag") ? resolved["agent_flag"] : "--agent"
598
598
  cmd = [resolved["agent_cli"]]
599
+ # cwd_flag: pass the working directory as a CLI argument (e.g. -C for Codex CLI).
600
+ cmd.push(resolved["cwd_flag"], project["repo_path"]) if resolved["cwd_flag"] && project["repo_path"]
599
601
  cmd.push(agent_flag, agent_config_name) if agent_flag
600
602
  cmd.concat(resolved["agent_cli_args"].split)
601
603
  cmd.push(resolved["agent_model_flag"], job[:model]) if resolved["agent_model_flag"]&.length&.positive? && job[:model]
602
- cmd.push(resolved["agent_effort_flag"], job[:effort]) if resolved["agent_effort_flag"]&.length&.positive? && job[:effort]
604
+ append_effort_to_cmd(cmd, job[:effort], resolved)
603
605
  cmd.push(resolved["prompt_flag"], prompt_file) if prompt_file && resolved["prompt_mode"] == "flag" && resolved["prompt_flag"]
604
606
  cmd
605
607
  end