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 +4 -4
- data/README.md +30 -3
- data/lib/brainiac/ask.rb +95 -0
- data/lib/brainiac/config.rb +7 -0
- data/lib/brainiac/helpers.rb +5 -3
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac.rb +1 -0
- data/receiver.rb +1 -0
- data/templates/brainiac.json.example +1 -0
- data/templates/cli-providers/kiro.json.example +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c65982d3a8149b7e0378221a00612218c0d77df2fe0c82370aa7210210f125d3
|
|
4
|
+
data.tar.gz: 4fcccf1bb797b144f6382a3562e5837ada64af5d1fc3ef3abc13b8f68a3d6818
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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"
|
data/lib/brainiac/ask.rb
ADDED
|
@@ -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
|
data/lib/brainiac/config.rb
CHANGED
|
@@ -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
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -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 >
|
|
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
|
|
data/lib/brainiac/version.rb
CHANGED
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"
|
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
|
|
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
|