brainiac 0.1.10 → 0.2.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: e63254b890e44045e1c05872417234f86e34fdcd36a737c7565c4ee7efc74426
4
- data.tar.gz: 630399ccb91dc4ef99b6f14619710c5893b6194354dc3a89f3d491cc4b226bdb
3
+ metadata.gz: c65982d3a8149b7e0378221a00612218c0d77df2fe0c82370aa7210210f125d3
4
+ data.tar.gz: 4fcccf1bb797b144f6382a3562e5837ada64af5d1fc3ef3abc13b8f68a3d6818
5
5
  SHA512:
6
- metadata.gz: d0a1f9cc0ca39aa87aec48c3b45132b7777e6dc6ca7f71006a78e6ebe00879c188d8cea9104822d2e32a3d1b65970d09d5271c558d5af10faa8d29a24bd182a5
7
- data.tar.gz: '02884d9354a9b5946a28f58f0e0c70d4f98893554b773f1738aa6e2a9383850d4f7577c8d691c9ef11867f71a81c5327a5ac28922fc7b90a95311bb1ceb536c4'
6
+ metadata.gz: 82764530696b3843f4723f6169e3026fb9540c5e78c833bc66cbb8642851bc70496404de3479426dc3a624197903735a4cd0b210396e06b7e81076a37467561a
7
+ data.tar.gz: 29e3a1a6c736f0f2cbb82908262f049aaddd5e59ded8eafb21854eae5d7f8cb83ea63c3e58df7b46bc165e0b9e681d3e4f696f09ef9a0a8994eba829b4b36497
data/README.md CHANGED
@@ -202,7 +202,7 @@ After setup, `~/.brainiac/` looks like:
202
202
 
203
203
  ```
204
204
  ~/.brainiac/
205
- ├── brainiac.json # Core config (default_agent)
205
+ ├── brainiac.json # Core config (default_agent, default_cli_provider)
206
206
  ├── agents.json # Agent registry (tokens, display names, local flag)
207
207
  ├── projects.json # Registered projects
208
208
  ├── users.json # Cross-platform user identity registry
@@ -262,6 +262,31 @@ The `agent_flag` field controls how agent identity is passed to the CLI. Differe
262
262
 
263
263
  Set `agent_flag` to `null` to suppress passing the agent name entirely (for CLIs that don't support persona switching).
264
264
 
265
+ `readonly_args` is optional. It replaces `default_args` when a plugin asks an agent something with `readonly: true` (see [Asking an agent from a plugin](#asking-an-agent-from-a-plugin)), so it must only allow tools that read:
266
+
267
+ | CLI | `readonly_args` |
268
+ |-----|-----------------|
269
+ | Kiro CLI | `chat --no-interactive --trust-tools=fs_read` |
270
+ | Claude Code | `-p --restricted --strict-mcp-config --no-session-persistence --tools Read,Grep,Glob` |
271
+
272
+ A provider without `readonly_args` can't be used read-only; the call fails instead of falling back to `default_args`.
273
+
274
+ Which provider runs is decided in this order: a `[cli:X]` tag, the agent's `cli_provider` in `agents.json`, the project's `cli_provider`, then `default_cli_provider` in `brainiac.json`. With none of those set, brainiac uses the `AGENT_CLI` env var, or kiro-cli.
275
+
276
+ #### Asking an agent from a plugin
277
+
278
+ `run_agent` starts a session and returns straight away. When a plugin needs the agent's answer back (for example to decide whether an incoming report is a bug), it calls `ask_agent`, which waits and returns the reply text:
279
+
280
+ ```ruby
281
+ reply = ask_agent(prompt, project_config: PROJECTS["my-app"], project_key: "my-app",
282
+ readonly: true, topic: "bug triage", timeout: 300)
283
+ ```
284
+
285
+ - The agent and CLI resolve the same way as `run_agent`: `agent_name:`, else the project's `agent_name`, else the default agent.
286
+ - `brain: true` (the default) puts the agent's role, persona and brain knowledge ahead of the prompt. `topic:` seeds the knowledge search.
287
+ - `readonly: true` runs the provider's `readonly_args`. Use it whenever the prompt contains text from outside (webhooks, emails, public forms).
288
+ - It raises `AskAgentError` on a timeout, a CLI failure, or a read-only request to a provider without `readonly_args`.
289
+
265
290
  Brainiac ships with example providers during setup. Your project config references which provider to use.
266
291
 
267
292
  #### 3. Environment Variables
@@ -269,10 +294,12 @@ Brainiac ships with example providers during setup. Your project config referenc
269
294
  Most config lives in JSON files now. The default agent is set in `brainiac.json`:
270
295
 
271
296
  ```json
272
- { "default_agent": "Sherlock" }
297
+ { "default_agent": "Sherlock", "default_cli_provider": "kiro" }
273
298
  ```
274
299
 
275
- Or via environment variable (takes precedence):
300
+ `default_cli_provider` is the CLI used when neither the agent nor the project names one (see [CLI Providers](#2-cli-providers-brainiaccli-providers)).
301
+
302
+ The default agent can also be set via environment variable (takes precedence):
276
303
 
277
304
  ```bash
278
305
  export AI_AGENT_NAME="Sherlock"
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ask_agent: a synchronous, one-off agent call for plugins that need the answer back
4
+ # (e.g. deciding whether an incoming report is a bug). run_agent spawns a session and
5
+ # returns before there's any output, so it can't do this.
6
+
7
+ class AskAgentError < StandardError; end
8
+
9
+ ANSI_ESCAPE = %r{\e\[[0-9;?]*[ -/]*[@-~]}
10
+
11
+ # Ask an agent a question and return its reply as text.
12
+ #
13
+ # The agent and CLI resolve the same way run_agent's do: agent_name, else the project's
14
+ # agent_name, else the default agent; cli_provider, else the agent's, the project's, then
15
+ # brainiac.json's default_cli_provider.
16
+ #
17
+ # brain: true puts the agent's role, persona and brain knowledge ahead of the prompt, so the
18
+ # whole agent answers rather than a bare model. topic seeds the knowledge search.
19
+ # readonly: true runs the provider's readonly_args instead of default_args. Raises
20
+ # AskAgentError when the provider has none: text from strangers must never reach an agent
21
+ # that can write files or run commands, so there's no falling back to default_args.
22
+ #
23
+ # Raises AskAgentError when the CLI times out or exits non-zero without a reply.
24
+ def ask_agent(prompt, project_config:, agent_name: nil, chdir: nil, readonly: false, brain: true, topic: "",
25
+ project_key: nil, model: nil, cli_provider: nil, timeout: 300, log_name: "ask")
26
+ agent_name ||= project_config["agent_name"] || AI_AGENT_NAME
27
+ resolved = ask_agent_cli_config(project_config, cli_provider: cli_provider, agent_name: agent_name, readonly: readonly)
28
+ chdir ||= resolved["repo_path"]
29
+ raise AskAgentError, "working directory #{chdir.inspect} doesn't exist" unless chdir && Dir.exist?(chdir)
30
+
31
+ if brain
32
+ context = build_brain_context(agent_name: agent_name, card_title: topic, project_key: project_key)
33
+ prompt = "#{context}\n## Task\n\n#{prompt}" unless context.empty?
34
+ end
35
+
36
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
37
+ prompt_file = write_agent_prompt_file(prompt, log_name, timestamp)
38
+ output_file = prepare_output_file(resolved, log_name, timestamp)
39
+ cmd = build_agent_cmd(resolved, agent_config_name: agent_name.downcase.gsub(/[^a-z0-9-]/, "-"),
40
+ model: model || resolved["agent_model"], prompt_file: prompt_file,
41
+ output_file: output_file, chdir: chdir)
42
+ LOG.info "[Ask] #{agent_name} via #{resolved["agent_cli"]}#{" (read-only)" if readonly} in #{chdir}: #{cmd.join(" ")}"
43
+
44
+ stdout = run_ask_command(cmd, prompt_file, resolved, chdir: chdir, env: profile_spawn_env(agent_env_for(agent_name), nil),
45
+ timeout: timeout)
46
+ (read_output_file(output_file) || stdout).gsub(ANSI_ESCAPE, "").strip
47
+ ensure
48
+ FileUtils.rm_f(prompt_file) if prompt_file
49
+ end
50
+
51
+ # The resolved CLI config, with readonly_args swapped in for default_args when asked.
52
+ def ask_agent_cli_config(project_config, cli_provider:, agent_name:, readonly:)
53
+ resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
54
+ return resolved unless readonly
55
+
56
+ args = resolved["readonly_args"]
57
+ if args.to_s.strip.empty?
58
+ raise AskAgentError, "#{resolved["agent_cli"]} has no readonly_args in its cli-providers config, " \
59
+ "so it can't be run read-only"
60
+ end
61
+
62
+ resolved.merge("agent_cli_args" => args)
63
+ end
64
+
65
+ # Run the CLI and return its stdout. Kills the whole process group on timeout so the
66
+ # CLI's own children (tool calls, MCP servers) go with it.
67
+ def run_ask_command(cmd, prompt_file, resolved, chdir:, env:, timeout:)
68
+ stdin_data = resolved["prompt_mode"] == "flag" ? "" : File.read(prompt_file)
69
+ Open3.popen3(env, *cmd, chdir: chdir, pgroup: true) do |stdin, stdout, stderr, wait|
70
+ readers = [Thread.new { stdout.read }, Thread.new { stderr.read }]
71
+ begin
72
+ stdin.write(stdin_data)
73
+ rescue Errno::EPIPE
74
+ nil # CLI exited before reading the prompt; its exit status says why.
75
+ end
76
+ stdin.close
77
+
78
+ unless wait.join(timeout)
79
+ begin
80
+ Process.kill("TERM", -wait.pid)
81
+ rescue Errno::ESRCH
82
+ nil # Finished just as the clock ran out.
83
+ end
84
+ readers.each(&:join) # The kill closes the pipes, so these return.
85
+ raise AskAgentError, "#{resolved["agent_cli"]} timed out after #{timeout}s"
86
+ end
87
+
88
+ out, err = readers.map(&:value)
89
+ if !wait.value.success? && out.strip.empty?
90
+ raise AskAgentError, "#{resolved["agent_cli"]} exited #{wait.value.exitstatus}: #{err.strip.lines.last(5).join}"
91
+ end
92
+
93
+ out
94
+ end
95
+ end
@@ -80,6 +80,13 @@ end
80
80
 
81
81
  AI_AGENT_NAME = resolve_default_agent
82
82
 
83
+ # --- Default CLI provider ---
84
+ # Last fallback for which CLI runs an agent, after the agent's and the project's
85
+ # cli_provider. Unset → DEFAULT_PROJECT's AGENT_CLI env var / kiro-cli.
86
+ def default_cli_provider
87
+ BRAINIAC_CONFIG["default_cli_provider"]
88
+ end
89
+
83
90
  LOG_LEVEL = ENV.fetch("LOG_LEVEL", "info").downcase
84
91
  LOG = Logger.new($stdout)
85
92
  LOG.level = case LOG_LEVEL
@@ -32,7 +32,7 @@ def load_cli_provider(provider_name)
32
32
  config["prompt_mode"] = raw["prompt_mode"] || "stdin"
33
33
  # Copy optional fields from raw config when present.
34
34
  # Each field controls a specific CLI behavior — see comments in the template.
35
- %w[prompt_flag list_models_command resume_flag resume_args resume_id_flag new_session_id_flag
35
+ %w[prompt_flag readonly_args list_models_command resume_flag resume_args resume_id_flag new_session_id_flag
36
36
  session_list_command session_id_field session_directory_field session_updated_field
37
37
  session_list_path session_dir output_last_message_flag cwd_flag config_override_flag
38
38
  effort_config_key effort_map title_flag heaviness_probe settings_model_cmd].each do |key|
@@ -76,12 +76,14 @@ end
76
76
  require_relative "model_parser"
77
77
 
78
78
  # Resolve CLI config for a project by merging provider defaults with project overrides.
79
- # Priority: cli_provider_override > agent-level cli_provider > project-level cli_provider > DEFAULT_PROJECT
79
+ # Priority: cli_provider_override > agent-level cli_provider > project-level cli_provider >
80
+ # brainiac.json default_cli_provider > DEFAULT_PROJECT
80
81
  def resolve_project_cli_config(project_config, cli_provider_override: nil, agent_name: nil)
81
- # Determine which CLI provider to use (priority: override > agent > project)
82
+ # Determine which CLI provider to use (priority: override > agent > project > default)
82
83
  provider_name = cli_provider_override
83
84
  provider_name ||= agent_cli_provider_for(agent_name) if agent_name
84
85
  provider_name ||= project_config["cli_provider"]
86
+ provider_name ||= default_cli_provider
85
87
 
86
88
  provider_config = load_cli_provider(provider_name)
87
89
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.1.10"
5
+ VERSION = "0.2.1"
6
6
  end
data/lib/brainiac.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "brainiac/sessions"
9
9
  require_relative "brainiac/session_history"
10
10
  require_relative "brainiac/prompts"
11
11
  require_relative "brainiac/helpers"
12
+ require_relative "brainiac/ask"
12
13
  require_relative "brainiac/intent"
13
14
  require_relative "brainiac/cron"
14
15
  require_relative "brainiac/plugins"
data/receiver.rb CHANGED
@@ -22,6 +22,7 @@ require_relative "lib/brainiac/sessions"
22
22
  require_relative "lib/brainiac/session_history"
23
23
  require_relative "lib/brainiac/prompts"
24
24
  require_relative "lib/brainiac/helpers"
25
+ require_relative "lib/brainiac/ask"
25
26
  require_relative "lib/brainiac/notifications"
26
27
  require_relative "lib/brainiac/cron"
27
28
  require_relative "lib/brainiac/restart"
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "default_agent": "Sherlock",
3
+ "default_cli_provider": "kiro",
3
4
  "intent": {
4
5
  "enabled": false,
5
6
  "endpoint": "http://localhost:11434/api/generate",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "binary": "kiro-cli",
3
3
  "default_args": "chat --trust-all-tools --no-interactive",
4
+ "readonly_args": "chat --no-interactive --trust-tools=fs_read",
4
5
  "agent_flag": "--agent",
5
6
  "model_flag": "--model",
6
7
  "effort_flag": "--effort",
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.10
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -126,6 +126,7 @@ files:
126
126
  - docs/waybar-config.md
127
127
  - lib/brainiac.rb
128
128
  - lib/brainiac/agents.rb
129
+ - lib/brainiac/ask.rb
129
130
  - lib/brainiac/brain.rb
130
131
  - lib/brainiac/config.rb
131
132
  - lib/brainiac/cron.rb