anima-core 0.3.0 → 1.0.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 +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -1
data/lib/anima/installer.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "pathname"
|
|
|
7
7
|
module Anima
|
|
8
8
|
class Installer
|
|
9
9
|
DIRECTORIES = %w[
|
|
10
|
+
agents
|
|
11
|
+
skills
|
|
10
12
|
db
|
|
11
13
|
config/credentials
|
|
12
14
|
log
|
|
@@ -16,6 +18,7 @@ module Anima
|
|
|
16
18
|
].freeze
|
|
17
19
|
|
|
18
20
|
ANIMA_HOME = Pathname.new(File.expand_path("~/.anima")).freeze
|
|
21
|
+
TEMPLATE_DIR = File.expand_path("../../templates", __dir__).freeze
|
|
19
22
|
|
|
20
23
|
attr_reader :anima_home
|
|
21
24
|
|
|
@@ -26,7 +29,10 @@ module Anima
|
|
|
26
29
|
def run
|
|
27
30
|
say "Installing Anima to #{anima_home}..."
|
|
28
31
|
create_directories
|
|
32
|
+
create_soul_file
|
|
29
33
|
create_config_file
|
|
34
|
+
create_settings_config
|
|
35
|
+
create_mcp_config
|
|
30
36
|
generate_credentials
|
|
31
37
|
create_systemd_service
|
|
32
38
|
say "Installation complete. Brain is running. Connect with 'anima tui'."
|
|
@@ -42,6 +48,18 @@ module Anima
|
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
# Copies the soul template to ~/.anima/soul.md — the agent's
|
|
52
|
+
# self-authored identity file. Skips if the file already exists
|
|
53
|
+
# so agent-written content is never overwritten.
|
|
54
|
+
def create_soul_file
|
|
55
|
+
soul_path = anima_home.join("soul.md")
|
|
56
|
+
return if soul_path.exist?
|
|
57
|
+
|
|
58
|
+
template = File.join(TEMPLATE_DIR, "soul.md")
|
|
59
|
+
soul_path.write(File.read(template))
|
|
60
|
+
say " created #{soul_path}"
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
def create_config_file
|
|
46
64
|
config_path = anima_home.join("config", "anima.yml")
|
|
47
65
|
return if config_path.exist?
|
|
@@ -53,6 +71,156 @@ module Anima
|
|
|
53
71
|
say " created #{config_path}"
|
|
54
72
|
end
|
|
55
73
|
|
|
74
|
+
def create_settings_config
|
|
75
|
+
config_path = anima_home.join("config.toml")
|
|
76
|
+
return if config_path.exist?
|
|
77
|
+
|
|
78
|
+
config_path.write(<<~TOML)
|
|
79
|
+
# Anima Configuration
|
|
80
|
+
#
|
|
81
|
+
# Edit settings below to customize Anima's behavior.
|
|
82
|
+
# Changes take effect immediately — no restart needed.
|
|
83
|
+
|
|
84
|
+
# ─── LLM ───────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
[llm]
|
|
87
|
+
|
|
88
|
+
# Primary model for conversations.
|
|
89
|
+
model = "claude-sonnet-4-20250514"
|
|
90
|
+
|
|
91
|
+
# Lightweight model for fast tasks (e.g. session naming).
|
|
92
|
+
fast_model = "claude-haiku-4-5"
|
|
93
|
+
|
|
94
|
+
# Maximum tokens per LLM response.
|
|
95
|
+
max_tokens = 8192
|
|
96
|
+
|
|
97
|
+
# Maximum consecutive tool execution rounds per request.
|
|
98
|
+
max_tool_rounds = 25
|
|
99
|
+
|
|
100
|
+
# Context window budget — tokens reserved for conversation history.
|
|
101
|
+
# Set this based on your model's context window minus system prompt.
|
|
102
|
+
token_budget = 190_000
|
|
103
|
+
|
|
104
|
+
# ─── Timeouts (seconds) ─────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
[timeouts]
|
|
107
|
+
|
|
108
|
+
# LLM API request timeout.
|
|
109
|
+
api = 30
|
|
110
|
+
|
|
111
|
+
# Shell command execution timeout.
|
|
112
|
+
command = 30
|
|
113
|
+
|
|
114
|
+
# MCP server response timeout.
|
|
115
|
+
mcp_response = 60
|
|
116
|
+
|
|
117
|
+
# Web fetch request timeout.
|
|
118
|
+
web_request = 10
|
|
119
|
+
|
|
120
|
+
# ─── Shell ──────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
[shell]
|
|
123
|
+
|
|
124
|
+
# Maximum bytes of command output before truncation.
|
|
125
|
+
max_output_bytes = 100_000
|
|
126
|
+
|
|
127
|
+
# ─── Tools ──────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
[tools]
|
|
130
|
+
|
|
131
|
+
# Maximum file size for read/edit operations (bytes).
|
|
132
|
+
max_file_size = 10_485_760
|
|
133
|
+
|
|
134
|
+
# Maximum lines returned by the read tool.
|
|
135
|
+
max_read_lines = 2_000
|
|
136
|
+
|
|
137
|
+
# Maximum bytes returned by the read tool.
|
|
138
|
+
max_read_bytes = 50_000
|
|
139
|
+
|
|
140
|
+
# Maximum bytes from web GET responses.
|
|
141
|
+
max_web_response_bytes = 100_000
|
|
142
|
+
|
|
143
|
+
# ─── Environment ──────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
[environment]
|
|
146
|
+
|
|
147
|
+
# Files to scan for in the working directory (at root and up to project_files_max_depth subdirectories deep).
|
|
148
|
+
project_files = ["CLAUDE.md", "AGENTS.md", "README.md", "CONTRIBUTING.md"]
|
|
149
|
+
|
|
150
|
+
# Maximum directory depth for project file scanning.
|
|
151
|
+
project_files_max_depth = 3
|
|
152
|
+
|
|
153
|
+
# ─── GitHub ─────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
[github]
|
|
156
|
+
|
|
157
|
+
# Repository for agent feature requests (owner/repo format).
|
|
158
|
+
# Falls back to parsing git remote origin when unset.
|
|
159
|
+
repo = "hoblin/anima"
|
|
160
|
+
|
|
161
|
+
# Label applied to agent-created feature request issues.
|
|
162
|
+
label = "anima-wants"
|
|
163
|
+
|
|
164
|
+
# ─── Paths ─────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
[paths]
|
|
167
|
+
|
|
168
|
+
# The agent's self-authored identity file.
|
|
169
|
+
soul = "#{anima_home.join("soul.md")}"
|
|
170
|
+
|
|
171
|
+
# ─── Session ────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
[session]
|
|
174
|
+
|
|
175
|
+
# Regenerate session name every N messages.
|
|
176
|
+
name_generation_interval = 30
|
|
177
|
+
|
|
178
|
+
# ─── Analytical Brain ─────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
[analytical_brain]
|
|
181
|
+
|
|
182
|
+
# Maximum tokens per analytical brain response.
|
|
183
|
+
# Must accommodate multiple tool calls (rename + goals + skills + ready).
|
|
184
|
+
max_tokens = 4096
|
|
185
|
+
|
|
186
|
+
# Run the analytical brain synchronously before the main agent on user messages.
|
|
187
|
+
# Ensures activated skills are available for the current response.
|
|
188
|
+
blocking_on_user_message = true
|
|
189
|
+
|
|
190
|
+
# Run the analytical brain asynchronously after the main agent completes.
|
|
191
|
+
blocking_on_agent_message = false
|
|
192
|
+
|
|
193
|
+
# Number of recent events to include in the analytical brain's context window.
|
|
194
|
+
event_window = 20
|
|
195
|
+
TOML
|
|
196
|
+
say " created #{config_path}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def create_mcp_config
|
|
200
|
+
config_path = anima_home.join("mcp.toml")
|
|
201
|
+
return if config_path.exist?
|
|
202
|
+
|
|
203
|
+
config_path.write(<<~TOML)
|
|
204
|
+
# MCP server configuration
|
|
205
|
+
# Declare MCP servers here. Anima connects on startup and
|
|
206
|
+
# registers their tools alongside built-in ones.
|
|
207
|
+
#
|
|
208
|
+
# HTTP transport:
|
|
209
|
+
# [servers.example]
|
|
210
|
+
# transport = "http"
|
|
211
|
+
# url = "http://localhost:3000/mcp/v2"
|
|
212
|
+
# headers = { Authorization = "Bearer ${API_KEY}" }
|
|
213
|
+
#
|
|
214
|
+
# Stdio transport:
|
|
215
|
+
# [servers.example]
|
|
216
|
+
# transport = "stdio"
|
|
217
|
+
# command = "my-mcp-server"
|
|
218
|
+
# args = ["--verbose"]
|
|
219
|
+
# env = { API_KEY = "${API_KEY}" }
|
|
220
|
+
TOML
|
|
221
|
+
say " created #{config_path}"
|
|
222
|
+
end
|
|
223
|
+
|
|
56
224
|
def generate_credentials
|
|
57
225
|
require "active_support"
|
|
58
226
|
require "active_support/encrypted_configuration"
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
|
|
5
|
+
module Anima
|
|
6
|
+
# User-facing configuration backed by +~/.anima/config.toml+ with hot-reload.
|
|
7
|
+
#
|
|
8
|
+
# Reads the TOML config file on each access, re-parsing only when the file's
|
|
9
|
+
# mtime has changed. The config file is created by the installer with all
|
|
10
|
+
# values set — it is the single source of truth for all settings.
|
|
11
|
+
#
|
|
12
|
+
# Settings are grouped into sections that mirror the TOML file structure:
|
|
13
|
+
#
|
|
14
|
+
# [llm] — Model selection and response limits
|
|
15
|
+
# [timeouts] — Network and execution timeouts (seconds)
|
|
16
|
+
# [shell] — Shell command output limits
|
|
17
|
+
# [tools] — File and web tool limits
|
|
18
|
+
# [session] — Conversation behavior
|
|
19
|
+
#
|
|
20
|
+
# @example Reading a setting
|
|
21
|
+
# Anima::Settings.model #=> "claude-sonnet-4-20250514"
|
|
22
|
+
# Anima::Settings.api_timeout #=> 30
|
|
23
|
+
#
|
|
24
|
+
# @example Hot-reload (no restart needed)
|
|
25
|
+
# Anima::Settings.model #=> "claude-sonnet-4-20250514"
|
|
26
|
+
# # user edits ~/.anima/config.toml: model = "claude-haiku-4-5"
|
|
27
|
+
# Anima::Settings.model #=> "claude-haiku-4-5"
|
|
28
|
+
#
|
|
29
|
+
# @see Anima::Installer#create_settings_config creates the config file
|
|
30
|
+
module Settings
|
|
31
|
+
DEFAULT_PATH = File.expand_path("~/.anima/config.toml")
|
|
32
|
+
|
|
33
|
+
class MissingConfigError < StandardError; end
|
|
34
|
+
class MissingSettingError < StandardError; end
|
|
35
|
+
|
|
36
|
+
@config_path = nil
|
|
37
|
+
@config_cache = nil
|
|
38
|
+
@config_mtime = nil
|
|
39
|
+
@cache_mutex = Mutex.new
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Override config file path (for testing).
|
|
43
|
+
# Resets the cache so the next access reads from the new location.
|
|
44
|
+
#
|
|
45
|
+
# @param path [String, nil] custom path, or +nil+ to restore default
|
|
46
|
+
def config_path=(path)
|
|
47
|
+
@config_path = path
|
|
48
|
+
@config_cache = nil
|
|
49
|
+
@config_mtime = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String] active config file path
|
|
53
|
+
def config_path
|
|
54
|
+
@config_path || DEFAULT_PATH
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Resets to default path and clears cached config.
|
|
58
|
+
# Useful in test teardown.
|
|
59
|
+
def reset!
|
|
60
|
+
self.config_path = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ─── LLM ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
# Primary model for conversations.
|
|
66
|
+
# @return [String] Anthropic model identifier
|
|
67
|
+
def model = get("llm", "model")
|
|
68
|
+
|
|
69
|
+
# Lightweight model for fast tasks (e.g. session naming).
|
|
70
|
+
# @return [String] Anthropic model identifier
|
|
71
|
+
def fast_model = get("llm", "fast_model")
|
|
72
|
+
|
|
73
|
+
# Maximum tokens per LLM response.
|
|
74
|
+
# @return [Integer]
|
|
75
|
+
def max_tokens = get("llm", "max_tokens")
|
|
76
|
+
|
|
77
|
+
# Maximum consecutive tool execution rounds per LLM message.
|
|
78
|
+
# Prevents runaway tool loops.
|
|
79
|
+
# @return [Integer]
|
|
80
|
+
def max_tool_rounds = get("llm", "max_tool_rounds")
|
|
81
|
+
|
|
82
|
+
# Context window budget — tokens reserved for conversation history.
|
|
83
|
+
# Set this based on your model's context window minus system prompt.
|
|
84
|
+
# @return [Integer]
|
|
85
|
+
def token_budget = get("llm", "token_budget")
|
|
86
|
+
|
|
87
|
+
# ─── Timeouts (seconds) ────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
# LLM API request timeout.
|
|
90
|
+
# @return [Integer] seconds
|
|
91
|
+
def api_timeout = get("timeouts", "api")
|
|
92
|
+
|
|
93
|
+
# Shell command execution timeout.
|
|
94
|
+
# @return [Integer] seconds
|
|
95
|
+
def command_timeout = get("timeouts", "command")
|
|
96
|
+
|
|
97
|
+
# MCP server response timeout.
|
|
98
|
+
# @return [Integer] seconds
|
|
99
|
+
def mcp_response_timeout = get("timeouts", "mcp_response")
|
|
100
|
+
|
|
101
|
+
# Web fetch request timeout.
|
|
102
|
+
# @return [Integer] seconds
|
|
103
|
+
def web_request_timeout = get("timeouts", "web_request")
|
|
104
|
+
|
|
105
|
+
# ─── Shell ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
# Maximum bytes of command output before truncation.
|
|
108
|
+
# @return [Integer]
|
|
109
|
+
def max_output_bytes = get("shell", "max_output_bytes")
|
|
110
|
+
|
|
111
|
+
# ─── Tools ──────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
# Maximum file size for read/edit operations (bytes).
|
|
114
|
+
# @return [Integer]
|
|
115
|
+
def max_file_size = get("tools", "max_file_size")
|
|
116
|
+
|
|
117
|
+
# Maximum lines returned by the read tool.
|
|
118
|
+
# @return [Integer]
|
|
119
|
+
def max_read_lines = get("tools", "max_read_lines")
|
|
120
|
+
|
|
121
|
+
# Maximum bytes returned by the read tool.
|
|
122
|
+
# @return [Integer]
|
|
123
|
+
def max_read_bytes = get("tools", "max_read_bytes")
|
|
124
|
+
|
|
125
|
+
# Maximum bytes from web GET responses.
|
|
126
|
+
# @return [Integer]
|
|
127
|
+
def max_web_response_bytes = get("tools", "max_web_response_bytes")
|
|
128
|
+
|
|
129
|
+
# ─── Session ────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
# Regenerate session name every N messages.
|
|
132
|
+
# @return [Integer]
|
|
133
|
+
def name_generation_interval = get("session", "name_generation_interval")
|
|
134
|
+
|
|
135
|
+
# ─── Paths ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
# Path to the soul file — the agent's self-authored identity.
|
|
138
|
+
# @return [String] absolute path
|
|
139
|
+
def soul_path = get("paths", "soul")
|
|
140
|
+
|
|
141
|
+
# ─── Environment ──────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
# Filenames to scan for in the working directory.
|
|
144
|
+
# @return [Array<String>]
|
|
145
|
+
def project_files_whitelist = get("environment", "project_files")
|
|
146
|
+
|
|
147
|
+
# Maximum directory depth for project file scanning.
|
|
148
|
+
# @return [Integer]
|
|
149
|
+
def project_files_max_depth = get("environment", "project_files_max_depth")
|
|
150
|
+
|
|
151
|
+
# ─── GitHub ─────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
# Repository for feature requests (+owner/repo+ format).
|
|
154
|
+
# Falls back to parsing git remote origin when unset.
|
|
155
|
+
# @return [String]
|
|
156
|
+
def github_repo = get("github", "repo")
|
|
157
|
+
|
|
158
|
+
# Label applied to agent-created feature request issues.
|
|
159
|
+
# @return [String]
|
|
160
|
+
def github_label = get("github", "label")
|
|
161
|
+
|
|
162
|
+
# ─── Analytical Brain ─────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
# Maximum tokens per analytical brain response.
|
|
165
|
+
# @return [Integer]
|
|
166
|
+
def analytical_brain_max_tokens = get("analytical_brain", "max_tokens")
|
|
167
|
+
|
|
168
|
+
# Run the analytical brain synchronously before the main agent on user messages.
|
|
169
|
+
# @return [Boolean]
|
|
170
|
+
def analytical_brain_blocking_on_user_message = get("analytical_brain", "blocking_on_user_message")
|
|
171
|
+
|
|
172
|
+
# Run the analytical brain asynchronously after the main agent completes.
|
|
173
|
+
# @return [Boolean]
|
|
174
|
+
def analytical_brain_blocking_on_agent_message = get("analytical_brain", "blocking_on_agent_message")
|
|
175
|
+
|
|
176
|
+
# Number of recent events to include in the analytical brain's context window.
|
|
177
|
+
# @return [Integer]
|
|
178
|
+
def analytical_brain_event_window = get("analytical_brain", "event_window")
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Reads a setting from the config file.
|
|
183
|
+
# Raises if the config file is missing or the key is not defined.
|
|
184
|
+
#
|
|
185
|
+
# @param section [String] TOML section name (e.g. "llm")
|
|
186
|
+
# @param key [String] setting key within the section (e.g. "model")
|
|
187
|
+
# @return [Object] the configured value
|
|
188
|
+
# @raise [MissingConfigError] when config.toml does not exist
|
|
189
|
+
# @raise [MissingSettingError] when the requested key is not in config
|
|
190
|
+
def get(section, key)
|
|
191
|
+
value = config.dig(section, key)
|
|
192
|
+
if value.nil?
|
|
193
|
+
raise MissingSettingError,
|
|
194
|
+
"[#{section}] #{key} is not set in #{config_path}. Run `anima install` to create the config file."
|
|
195
|
+
end
|
|
196
|
+
value
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Loads the TOML config with mtime-based caching.
|
|
200
|
+
# Re-parses only when the file has been modified since the last read.
|
|
201
|
+
# Thread-safe via mutex — concurrent callers share the same cache.
|
|
202
|
+
#
|
|
203
|
+
# @return [Hash] parsed config
|
|
204
|
+
# @raise [MissingConfigError] when config.toml does not exist
|
|
205
|
+
def config
|
|
206
|
+
@cache_mutex.synchronize do
|
|
207
|
+
path = config_path
|
|
208
|
+
unless File.exist?(path)
|
|
209
|
+
@config_cache = nil
|
|
210
|
+
@config_mtime = nil
|
|
211
|
+
raise MissingConfigError,
|
|
212
|
+
"Config file not found: #{path}. Run `anima install` to create it."
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
current_mtime = File.mtime(path)
|
|
216
|
+
if current_mtime != @config_mtime
|
|
217
|
+
@config_mtime = current_mtime
|
|
218
|
+
@config_cache = TomlRB.load_file(path)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
@config_cache
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/anima/version.rb
CHANGED
data/lib/anima.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require_relative "anima/version"
|
|
5
|
+
require_relative "anima/settings"
|
|
5
6
|
|
|
6
7
|
module Anima
|
|
7
8
|
class Error < StandardError; end
|
|
@@ -9,4 +10,12 @@ module Anima
|
|
|
9
10
|
def self.gem_root
|
|
10
11
|
@gem_root ||= Pathname.new(File.expand_path("..", __dir__))
|
|
11
12
|
end
|
|
13
|
+
|
|
14
|
+
# Boots Rails when CLI commands need access to Rails-managed resources
|
|
15
|
+
# like encrypted credentials. No-op if Rails is already loaded.
|
|
16
|
+
def self.boot_rails!
|
|
17
|
+
return if defined?(Rails)
|
|
18
|
+
|
|
19
|
+
require gem_root.join("config", "environment").to_s
|
|
20
|
+
end
|
|
12
21
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Low-level read/write operations on Rails encrypted credentials.
|
|
4
|
+
# Wraps the merge-and-write pattern used by {SessionChannel#write_anthropic_token}
|
|
5
|
+
# in a reusable helper. All namespacing (e.g. +mcp+, +anthropic+) is the
|
|
6
|
+
# caller's responsibility — this class operates on raw top-level keys.
|
|
7
|
+
#
|
|
8
|
+
# @example Writing a nested credential
|
|
9
|
+
# CredentialStore.write("mcp", "linear_api_key" => "sk-xxx")
|
|
10
|
+
#
|
|
11
|
+
# @example Reading a nested credential
|
|
12
|
+
# CredentialStore.read("mcp", "linear_api_key") #=> "sk-xxx"
|
|
13
|
+
class CredentialStore
|
|
14
|
+
class << self
|
|
15
|
+
# Writes one or more key-value pairs under a top-level namespace.
|
|
16
|
+
# Merges into existing credentials, preserving sibling keys.
|
|
17
|
+
#
|
|
18
|
+
# @param namespace [String] top-level YAML key (e.g. "mcp", "anthropic")
|
|
19
|
+
# @param pairs [Hash<String, String>] key-value pairs to store
|
|
20
|
+
# @return [void]
|
|
21
|
+
def write(namespace, pairs)
|
|
22
|
+
existing = load_credentials
|
|
23
|
+
section = existing[namespace] ||= {}
|
|
24
|
+
section.merge!(pairs)
|
|
25
|
+
save_credentials(existing)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Reads a single credential value from a namespace.
|
|
29
|
+
# Busts the Rails credentials cache first so cross-process writes
|
|
30
|
+
# (e.g. token saved in the web process, read in the SolidQueue worker)
|
|
31
|
+
# are always visible.
|
|
32
|
+
#
|
|
33
|
+
# @param namespace [String] top-level YAML key
|
|
34
|
+
# @param key [String] credential key within the namespace
|
|
35
|
+
# @return [String, nil] credential value or nil if not found
|
|
36
|
+
def read(namespace, key)
|
|
37
|
+
bust_credentials_cache!
|
|
38
|
+
Rails.application.credentials.dig(namespace.to_sym, key.to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Lists all keys under a namespace.
|
|
42
|
+
#
|
|
43
|
+
# @param namespace [String] top-level YAML key
|
|
44
|
+
# @return [Array<String>] credential keys (not values)
|
|
45
|
+
def list(namespace)
|
|
46
|
+
section = Rails.application.credentials.dig(namespace.to_sym)
|
|
47
|
+
return [] unless section.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
section.keys.map(&:to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Removes a single key from a namespace.
|
|
53
|
+
# No-op if the key does not exist.
|
|
54
|
+
#
|
|
55
|
+
# @param namespace [String] top-level YAML key
|
|
56
|
+
# @param key [String] credential key to remove
|
|
57
|
+
# @return [void]
|
|
58
|
+
def remove(namespace, key)
|
|
59
|
+
existing = load_credentials
|
|
60
|
+
section = existing[namespace]
|
|
61
|
+
return unless section.is_a?(Hash)
|
|
62
|
+
return unless section.key?(key)
|
|
63
|
+
|
|
64
|
+
section.delete(key)
|
|
65
|
+
existing.delete(namespace) if section.empty?
|
|
66
|
+
save_credentials(existing)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Reads and parses the raw YAML from encrypted credentials.
|
|
72
|
+
# Returns an empty hash when the credentials file does not exist yet.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash] parsed credentials
|
|
75
|
+
def load_credentials
|
|
76
|
+
creds = Rails.application.credentials
|
|
77
|
+
YAML.safe_load(creds.read) || {}
|
|
78
|
+
rescue ActiveSupport::EncryptedFile::MissingContentError
|
|
79
|
+
{}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Writes the full credentials hash back to the encrypted file and
|
|
83
|
+
# invalidates the Rails memoization cache so subsequent reads see
|
|
84
|
+
# fresh data.
|
|
85
|
+
#
|
|
86
|
+
# @param data [Hash] complete credentials hash to persist
|
|
87
|
+
# @return [void]
|
|
88
|
+
def save_credentials(data)
|
|
89
|
+
creds = Rails.application.credentials
|
|
90
|
+
creds.write(data.to_yaml)
|
|
91
|
+
bust_credentials_cache!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clears the Rails credentials in-memory cache so the next access
|
|
95
|
+
# re-reads and decrypts from disk. Required because Rails memoizes
|
|
96
|
+
# the decrypted config in @config per-process.
|
|
97
|
+
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
def bust_credentials_cache!
|
|
100
|
+
Rails.application.credentials.instance_variable_set(:@config, nil)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|