Class: Anima::Installer

Inherits:
Object
  • Object
show all
Defined in:
lib/anima/installer.rb

Constant Summary collapse

DIRECTORIES =
%w[
  agents
  skills
  db
  config/credentials
  log
  tmp
  tmp/pids
  tmp/cache
].freeze
ANIMA_HOME =
Pathname.new(File.expand_path("~/.anima")).freeze
TEMPLATE_DIR =
File.expand_path("../../templates", __dir__).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(anima_home: ANIMA_HOME) ⇒ Installer

Returns a new instance of Installer.



25
26
27
# File 'lib/anima/installer.rb', line 25

def initialize(anima_home: ANIMA_HOME)
  @anima_home = anima_home
end

Instance Attribute Details

#anima_homeObject (readonly)

Returns the value of attribute anima_home.



23
24
25
# File 'lib/anima/installer.rb', line 23

def anima_home
  @anima_home
end

Instance Method Details

#create_config_fileObject



63
64
65
66
67
68
69
70
71
72
# File 'lib/anima/installer.rb', line 63

def create_config_file
  config_path = anima_home.join("config", "anima.yml")
  return if config_path.exist?

  config_path.write(<<~YAML)
    # Anima configuration
    # See https://github.com/hoblin/anima for documentation
  YAML
  say "  created #{config_path}"
end

#create_directoriesObject



41
42
43
44
45
46
47
48
49
# File 'lib/anima/installer.rb', line 41

def create_directories
  DIRECTORIES.each do |dir|
    path = anima_home.join(dir)
    next if path.directory?

    FileUtils.mkdir_p(path)
    say "  created #{path}"
  end
end

#create_mcp_configObject



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/anima/installer.rb', line 199

def create_mcp_config
  config_path = anima_home.join("mcp.toml")
  return if config_path.exist?

  config_path.write(<<~TOML)
    # MCP server configuration
    # Declare MCP servers here. Anima connects on startup and
    # registers their tools alongside built-in ones.
    #
    # HTTP transport:
    # [servers.example]
    # transport = "http"
    # url = "http://localhost:3000/mcp/v2"
    # headers = { Authorization = "Bearer ${API_KEY}" }
    #
    # Stdio transport:
    # [servers.example]
    # transport = "stdio"
    # command = "my-mcp-server"
    # args = ["--verbose"]
    # env = { API_KEY = "${API_KEY}" }
  TOML
  say "  created #{config_path}"
end

#create_settings_configObject



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/anima/installer.rb', line 74

def create_settings_config
  config_path = anima_home.join("config.toml")
  return if config_path.exist?

  config_path.write(<<~TOML)
    # Anima Configuration
    #
    # Edit settings below to customize Anima's behavior.
    # Changes take effect immediately — no restart needed.

    # ─── LLM ───────────────────────────────────────────────────────

    [llm]

    # Primary model for conversations.
    model = "claude-sonnet-4-20250514"

    # Lightweight model for fast tasks (e.g. session naming).
    fast_model = "claude-haiku-4-5"

    # Maximum tokens per LLM response.
    max_tokens = 8192

    # Maximum consecutive tool execution rounds per request.
    max_tool_rounds = 25

    # Context window budget — tokens reserved for conversation history.
    # Set this based on your model's context window minus system prompt.
    token_budget = 190_000

    # ─── Timeouts (seconds) ─────────────────────────────────────────

    [timeouts]

    # LLM API request timeout.
    api = 30

    # Shell command execution timeout.
    command = 30

    # MCP server response timeout.
    mcp_response = 60

    # Web fetch request timeout.
    web_request = 10

    # ─── Shell ──────────────────────────────────────────────────────

    [shell]

    # Maximum bytes of command output before truncation.
    max_output_bytes = 100_000

    # ─── Tools ──────────────────────────────────────────────────────

    [tools]

    # Maximum file size for read/edit operations (bytes).
    max_file_size = 10_485_760

    # Maximum lines returned by the read tool.
    max_read_lines = 2_000

    # Maximum bytes returned by the read tool.
    max_read_bytes = 50_000

    # Maximum bytes from web GET responses.
    max_web_response_bytes = 100_000

    # ─── Environment ──────────────────────────────────────────────

    [environment]

    # Files to scan for in the working directory (at root and up to project_files_max_depth subdirectories deep).
    project_files = ["CLAUDE.md", "AGENTS.md", "README.md", "CONTRIBUTING.md"]

    # Maximum directory depth for project file scanning.
    project_files_max_depth = 3

    # ─── GitHub ─────────────────────────────────────────────────────

    [github]

    # Repository for agent feature requests (owner/repo format).
    # Falls back to parsing git remote origin when unset.
    repo = "hoblin/anima"

    # Label applied to agent-created feature request issues.
    label = "anima-wants"

    # ─── Paths ─────────────────────────────────────────────────────

    [paths]

    # The agent's self-authored identity file.
    soul = "#{anima_home.join("soul.md")}"

    # ─── Session ────────────────────────────────────────────────────

    [session]

    # Regenerate session name every N messages.
    name_generation_interval = 30

    # ─── Analytical Brain ─────────────────────────────────────────

    [analytical_brain]

    # Maximum tokens per analytical brain response.
    # Must accommodate multiple tool calls (rename + goals + skills + ready).
    max_tokens = 4096

    # Run the analytical brain synchronously before the main agent on user messages.
    # Ensures activated skills are available for the current response.
    blocking_on_user_message = true

    # Run the analytical brain asynchronously after the main agent completes.
    blocking_on_agent_message = false

    # Number of recent events to include in the analytical brain's context window.
    event_window = 20
  TOML
  say "  created #{config_path}"
end

#create_soul_fileObject

Copies the soul template to ~/.anima/soul.md — the agent’s self-authored identity file. Skips if the file already exists so agent-written content is never overwritten.



54
55
56
57
58
59
60
61
# File 'lib/anima/installer.rb', line 54

def create_soul_file
  soul_path = anima_home.join("soul.md")
  return if soul_path.exist?

  template = File.join(TEMPLATE_DIR, "soul.md")
  soul_path.write(File.read(template))
  say "  created #{soul_path}"
end

#create_systemd_serviceObject



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/anima/installer.rb', line 251

def create_systemd_service
  service_dir = Pathname.new(File.expand_path("~/.config/systemd/user"))
  service_path = service_dir.join("anima.service")
  FileUtils.mkdir_p(service_dir)

  anima_bin = File.join(Gem.bindir, "anima")
  unit_content = <<~UNIT
    [Unit]
    Description=Anima - Personal AI Agent
    After=network.target

    [Service]
    Type=simple
    ExecStart=#{anima_bin} start -e production
    Restart=on-failure
    RestartSec=5

    [Install]
    WantedBy=default.target
  UNIT

  if service_path.exist?
    if service_path.read == unit_content
      say "  anima.service unchanged"
    else
      service_path.write(unit_content)
      say "  updated #{service_path}"
    end
  else
    service_path.write(unit_content)
    say "  created #{service_path}"
  end

  system("systemctl", "--user", "daemon-reload", err: File::NULL, out: File::NULL)
  system("systemctl", "--user", "enable", "--now", "anima.service", err: File::NULL, out: File::NULL)
  say "  enabled and started anima.service"
end

#generate_credentialsObject



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/anima/installer.rb', line 224

def generate_credentials
  require "active_support"
  require "active_support/encrypted_configuration"

  %w[production development test].each do |env|
    content_path = anima_home.join("config", "credentials", "#{env}.yml.enc")
    key_path = anima_home.join("config", "credentials", "#{env}.key")

    next if key_path.exist? && content_path.exist?

    key = ActiveSupport::EncryptedFile.generate_key
    key_path.write(key)
    File.chmod(0o600, key_path.to_s)

    config = ActiveSupport::EncryptedConfiguration.new(
      config_path: content_path.to_s,
      key_path: key_path.to_s,
      env_key: "RAILS_MASTER_KEY",
      raise_if_missing_key: true
    )

    config.write("secret_key_base: #{SecureRandom.hex(64)}\n")
    File.chmod(0o600, content_path.to_s)
    say "  created credentials for #{env}"
  end
end

#runObject



29
30
31
32
33
34
35
36
37
38
39
# File 'lib/anima/installer.rb', line 29

def run
  say "Installing Anima to #{anima_home}..."
  create_directories
  create_soul_file
  create_config_file
  create_settings_config
  create_mcp_config
  generate_credentials
  create_systemd_service
  say "Installation complete. Brain is running. Connect with 'anima tui'."
end