anima-core 1.2.0 → 1.4.0
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/.reek.yml +14 -8
- data/README.md +96 -23
- data/agents/codebase-analyzer.md +1 -1
- data/agents/codebase-pattern-finder.md +1 -1
- data/agents/documentation-researcher.md +1 -1
- data/agents/thoughts-analyzer.md +1 -1
- data/agents/web-search-researcher.md +2 -2
- data/app/channels/session_channel.rb +53 -35
- data/app/decorators/tool_call_decorator.rb +7 -7
- data/app/decorators/user_message_decorator.rb +3 -17
- data/app/jobs/agent_request_job.rb +15 -6
- data/app/jobs/passive_recall_job.rb +6 -11
- data/app/models/concerns/message/broadcasting.rb +1 -0
- data/app/models/goal.rb +14 -0
- data/app/models/message.rb +13 -31
- data/app/models/pending_message.rb +191 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +480 -271
- data/bin/inspect-cassette +144 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/database.yml +1 -0
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/cable_structure.sql +9 -0
- data/db/migrate/20260328100000_create_secrets.rb +15 -0
- data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
- data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
- data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
- data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
- data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
- data/db/queue_structure.sql +61 -0
- data/db/structure.sql +120 -0
- data/lib/agent_loop.rb +53 -51
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +19 -6
- data/lib/analytical_brain/tools/activate_skill.rb +2 -2
- data/lib/analytical_brain/tools/assign_nickname.rb +1 -1
- data/lib/analytical_brain/tools/deactivate_skill.rb +2 -1
- data/lib/analytical_brain/tools/deactivate_workflow.rb +2 -1
- data/lib/analytical_brain/tools/finish_goal.rb +3 -0
- data/lib/analytical_brain/tools/goal_messaging.rb +28 -0
- data/lib/analytical_brain/tools/read_workflow.rb +2 -2
- data/lib/analytical_brain/tools/set_goal.rb +5 -1
- data/lib/analytical_brain/tools/update_goal.rb +5 -1
- data/lib/anima/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/cli.rb +41 -13
- data/lib/anima/installer.rb +20 -1
- data/lib/anima/settings.rb +37 -2
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/agent_message.rb +14 -0
- data/lib/events/base.rb +1 -1
- data/lib/events/subscribers/persister.rb +12 -18
- data/lib/events/subscribers/subagent_message_router.rb +18 -9
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +91 -50
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- data/lib/mneme/compressed_viewport.rb +9 -5
- data/lib/mneme/passive_recall.rb +85 -16
- data/lib/mneme/runner.rb +15 -4
- data/lib/providers/anthropic.rb +112 -7
- data/lib/shell_session.rb +239 -18
- data/lib/tools/base.rb +22 -0
- data/lib/tools/bash.rb +61 -7
- data/lib/tools/edit.rb +2 -2
- data/lib/tools/mark_goal_completed.rb +85 -0
- data/lib/tools/read.rb +2 -1
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +41 -7
- data/lib/tools/remember.rb +1 -1
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +11 -8
- data/lib/tools/spawn_subagent.rb +19 -13
- data/lib/tools/subagent_prompts.rb +41 -5
- data/lib/tools/think.rb +23 -0
- data/lib/tools/write.rb +1 -1
- data/lib/tui/app.rb +545 -137
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +13 -20
- data/lib/tui/decorators/base_decorator.rb +40 -11
- data/lib/tui/decorators/bash_decorator.rb +3 -3
- data/lib/tui/decorators/edit_decorator.rb +7 -4
- data/lib/tui/decorators/read_decorator.rb +6 -8
- data/lib/tui/decorators/think_decorator.rb +4 -6
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +7 -4
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +33 -0
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +159 -27
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +302 -103
- data/lib/tui/settings.rb +86 -0
- data/skills/activerecord/SKILL.md +1 -1
- data/skills/dragonruby/SKILL.md +1 -1
- data/skills/draper-decorators/SKILL.md +1 -1
- data/skills/gh-issue.md +1 -1
- data/skills/mcp-server/SKILL.md +1 -1
- data/skills/ratatui-ruby/SKILL.md +1 -1
- data/skills/rspec/SKILL.md +1 -1
- data/templates/config.toml +30 -1
- data/templates/tui.toml +209 -0
- metadata +24 -3
- data/config/initializers/fts5_schema_dump.rb +0 -21
- data/lib/environment_probe.rb +0 -232
data/lib/environment_probe.rb
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "etc"
|
|
4
|
-
require "open3"
|
|
5
|
-
require "timeout"
|
|
6
|
-
require "json"
|
|
7
|
-
require "pathname"
|
|
8
|
-
require "uri"
|
|
9
|
-
|
|
10
|
-
# Probes the shell environment and assembles a lightweight metadata block
|
|
11
|
-
# for injection into the system prompt. Gives the agent awareness of its
|
|
12
|
-
# working directory, OS, Git status, and nearby project files — without
|
|
13
|
-
# loading any file content.
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# EnvironmentProbe.to_prompt("/home/user/projects/my-app")
|
|
17
|
-
# # => "## Environment\n\nOS: Arch Linux (pacman, yay)\n..."
|
|
18
|
-
class EnvironmentProbe
|
|
19
|
-
# Assembles the environment context block for a given working directory.
|
|
20
|
-
#
|
|
21
|
-
# @param pwd [String, nil] current working directory
|
|
22
|
-
# @return [String, nil] Markdown-formatted environment block, or nil when pwd is unknown
|
|
23
|
-
def self.to_prompt(pwd)
|
|
24
|
-
new(pwd).to_prompt
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# @param pwd [String, nil] current working directory
|
|
28
|
-
def initialize(pwd)
|
|
29
|
-
@pwd = pwd
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# @return [String, nil] Markdown-formatted environment block
|
|
33
|
-
def to_prompt
|
|
34
|
-
return unless @pwd
|
|
35
|
-
|
|
36
|
-
sections = [os_section, working_directory_section, project_files_section].compact
|
|
37
|
-
return if sections.empty?
|
|
38
|
-
|
|
39
|
-
"## Environment\n\n#{sections.join("\n\n")}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
# @return [String] OS name with package manager hint
|
|
45
|
-
def os_section
|
|
46
|
-
sysname = Etc.uname[:sysname]
|
|
47
|
-
"OS: #{format_os(sysname)}"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# @param sysname [String] kernel name from uname (e.g. "Linux", "Darwin")
|
|
51
|
-
# @return [String] human-readable OS description
|
|
52
|
-
def format_os(sysname)
|
|
53
|
-
case sysname
|
|
54
|
-
when "Linux"
|
|
55
|
-
distro = detect_linux_distro || "Linux"
|
|
56
|
-
pkg = detect_package_manager
|
|
57
|
-
pkg ? "#{distro} (#{pkg})" : distro
|
|
58
|
-
when "Darwin"
|
|
59
|
-
"macOS (Homebrew)"
|
|
60
|
-
else
|
|
61
|
-
sysname
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Reads PRETTY_NAME from /etc/os-release.
|
|
66
|
-
#
|
|
67
|
-
# @return [String, nil] distro name, or nil on non-Linux / missing file
|
|
68
|
-
def detect_linux_distro
|
|
69
|
-
return unless File.exist?("/etc/os-release")
|
|
70
|
-
|
|
71
|
-
File.foreach("/etc/os-release") do |line|
|
|
72
|
-
if line.start_with?("PRETTY_NAME=")
|
|
73
|
-
return line.split("=", 2).last.strip.delete('"')
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
nil
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Returns the primary package manager(s) for the current system.
|
|
80
|
-
# Arch-based systems list both pacman and yay when present;
|
|
81
|
-
# other families return the first match.
|
|
82
|
-
#
|
|
83
|
-
# @return [String, nil] comma-separated package manager names
|
|
84
|
-
def detect_package_manager
|
|
85
|
-
managers = []
|
|
86
|
-
managers << "pacman" if File.exist?("/usr/bin/pacman")
|
|
87
|
-
managers << "yay" if File.exist?("/usr/bin/yay")
|
|
88
|
-
return managers.join(", ") if managers.any?
|
|
89
|
-
|
|
90
|
-
return "apt" if File.exist?("/usr/bin/apt")
|
|
91
|
-
return "dnf" if File.exist?("/usr/bin/dnf")
|
|
92
|
-
return "Homebrew" if File.exist?("/opt/homebrew/bin/brew") || File.exist?("/usr/local/bin/brew")
|
|
93
|
-
|
|
94
|
-
nil
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# @return [String] CWD line plus optional Git metadata
|
|
98
|
-
def working_directory_section
|
|
99
|
-
lines = ["CWD: #{@pwd}"]
|
|
100
|
-
append_git_lines(lines)
|
|
101
|
-
lines.join("\n")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Appends Git metadata lines (remote, branch, PR) to the output array.
|
|
105
|
-
#
|
|
106
|
-
# @param lines [Array<String>] accumulator for output lines
|
|
107
|
-
# @return [void]
|
|
108
|
-
def append_git_lines(lines)
|
|
109
|
-
git = detect_git
|
|
110
|
-
return unless git
|
|
111
|
-
|
|
112
|
-
remote = git[:remote]
|
|
113
|
-
branch = git[:branch]
|
|
114
|
-
pr_number = git[:pr_number]
|
|
115
|
-
|
|
116
|
-
lines << "Git: #{git[:repo_name]} (#{remote})" if remote
|
|
117
|
-
lines << "Branch: #{branch}" if branch
|
|
118
|
-
lines << "PR: ##{pr_number} (#{git[:pr_state]})" if pr_number
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Detects Git repo metadata: remote, branch, and open PR.
|
|
122
|
-
#
|
|
123
|
-
# @return [Hash{Symbol => String}, nil] keys: :remote, :repo_name, :branch,
|
|
124
|
-
# and optionally :pr_number (Integer) and :pr_state (String); nil when not in a repo
|
|
125
|
-
def detect_git
|
|
126
|
-
_, status = Open3.capture2("git", "-C", @pwd, "rev-parse", "--is-inside-work-tree", err: File::NULL)
|
|
127
|
-
return unless status.success?
|
|
128
|
-
|
|
129
|
-
info = {}
|
|
130
|
-
detect_git_remote(info)
|
|
131
|
-
detect_git_branch(info)
|
|
132
|
-
info
|
|
133
|
-
rescue Errno::ENOENT
|
|
134
|
-
nil
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Populates :remote and :repo_name on the info hash.
|
|
138
|
-
def detect_git_remote(info)
|
|
139
|
-
remote, = Open3.capture2("git", "-C", @pwd, "remote", "get-url", "origin", err: File::NULL)
|
|
140
|
-
remote = remote.strip
|
|
141
|
-
return unless remote.present?
|
|
142
|
-
|
|
143
|
-
info[:remote] = remote
|
|
144
|
-
info[:repo_name] = extract_repo_name(remote)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Populates :branch, :pr_number, and :pr_state on the info hash.
|
|
148
|
-
def detect_git_branch(info)
|
|
149
|
-
branch, = Open3.capture2("git", "-C", @pwd, "rev-parse", "--abbrev-ref", "HEAD", err: File::NULL)
|
|
150
|
-
branch = branch.strip
|
|
151
|
-
return unless branch.present?
|
|
152
|
-
|
|
153
|
-
info[:branch] = branch
|
|
154
|
-
pr = detect_pr(branch)
|
|
155
|
-
info.merge!(pr) if pr
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Extracts owner/repo from a Git remote URL.
|
|
159
|
-
#
|
|
160
|
-
# @param remote_url [String] SSH or HTTPS remote URL
|
|
161
|
-
# @return [String] "owner/repo" path, or the raw URL when parsing fails
|
|
162
|
-
def extract_repo_name(remote_url)
|
|
163
|
-
path = if remote_url.match?(%r{\A\w+://})
|
|
164
|
-
URI.parse(remote_url).path
|
|
165
|
-
else
|
|
166
|
-
# SSH format: git@host:owner/repo.git
|
|
167
|
-
remote_url.split(":").last
|
|
168
|
-
end
|
|
169
|
-
path.delete_prefix("/").delete_suffix(".git")
|
|
170
|
-
rescue URI::InvalidURIError
|
|
171
|
-
remote_url
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Queries GitHub for an open PR on the given branch via the gh CLI.
|
|
175
|
-
#
|
|
176
|
-
# @param branch [String] branch name
|
|
177
|
-
# @return [Hash, nil] with :pr_number and :pr_state, or nil
|
|
178
|
-
# @note Returns nil on timeout, missing gh CLI, or JSON parse errors
|
|
179
|
-
def detect_pr(branch)
|
|
180
|
-
Timeout.timeout(Anima::Settings.web_request_timeout) do
|
|
181
|
-
output, status = Open3.capture2(
|
|
182
|
-
"gh", "pr", "list", "--head", branch,
|
|
183
|
-
"--json", "number,state", "--limit", "1",
|
|
184
|
-
chdir: @pwd, err: File::NULL
|
|
185
|
-
)
|
|
186
|
-
return unless status.success?
|
|
187
|
-
|
|
188
|
-
pr = JSON.parse(output).first
|
|
189
|
-
return unless pr
|
|
190
|
-
|
|
191
|
-
{pr_number: pr["number"], pr_state: pr["state"].downcase}
|
|
192
|
-
end
|
|
193
|
-
rescue Timeout::Error, Errno::ENOENT, JSON::ParserError
|
|
194
|
-
nil
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Scans for well-known project files up to a configurable depth.
|
|
198
|
-
#
|
|
199
|
-
# @return [String, nil] project files section, or nil when none found
|
|
200
|
-
def project_files_section
|
|
201
|
-
found = scan_project_files
|
|
202
|
-
return if found.empty?
|
|
203
|
-
|
|
204
|
-
header = "Project files that may contain useful context:"
|
|
205
|
-
entries = found.map { |path| "- #{path}" }
|
|
206
|
-
[header, *entries, "Use read_file to examine these when needed."].join("\n")
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Scans the working directory for whitelisted filenames.
|
|
210
|
-
#
|
|
211
|
-
# @return [Array<String>] sorted relative paths
|
|
212
|
-
def scan_project_files
|
|
213
|
-
base = Pathname.new(@pwd)
|
|
214
|
-
|
|
215
|
-
glob_patterns.flat_map { |pattern| Dir.glob(pattern) }
|
|
216
|
-
.map { |full_path| Pathname.new(full_path).relative_path_from(base).to_s }
|
|
217
|
-
.sort
|
|
218
|
-
.uniq
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Builds glob patterns for each whitelisted filename at each depth level.
|
|
222
|
-
#
|
|
223
|
-
# @return [Array<String>] glob patterns
|
|
224
|
-
def glob_patterns
|
|
225
|
-
whitelist = Anima::Settings.project_files_whitelist
|
|
226
|
-
max_depth = Anima::Settings.project_files_max_depth
|
|
227
|
-
|
|
228
|
-
whitelist.product((0..max_depth).to_a).map do |filename, depth|
|
|
229
|
-
File.join(@pwd, Array.new(depth, "*"), filename)
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|