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.
Files changed (269) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +168 -0
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -1
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anima
4
- VERSION = "0.3.0"
4
+ VERSION = "1.0.0"
5
5
  end
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