anima-core 0.2.1 → 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 +19 -0
- data/README.md +213 -43
- 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 +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -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 -6
- 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 +5 -40
- 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/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -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 +11 -20
- 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 +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- 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/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- 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 +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Marks a goal as completed on the main session. Sets the status to
|
|
6
|
+
# "completed" and records the completion timestamp.
|
|
7
|
+
class FinishGoal < ::Tools::Base
|
|
8
|
+
def self.tool_name = "finish_goal"
|
|
9
|
+
|
|
10
|
+
def self.description = "Mark a goal as completed. " \
|
|
11
|
+
"Use this when the main agent has finished the work described by the goal."
|
|
12
|
+
|
|
13
|
+
def self.input_schema
|
|
14
|
+
{
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
goal_id: {
|
|
18
|
+
type: "integer",
|
|
19
|
+
description: "ID of the goal to mark as completed"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: %w[goal_id]
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param main_session [Session] the session owning the goal
|
|
27
|
+
def initialize(main_session:, **)
|
|
28
|
+
@main_session = main_session
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param input [Hash<String, Object>] with "goal_id"
|
|
32
|
+
# @return [String] confirmation message
|
|
33
|
+
# @return [Hash] with :error key on failure
|
|
34
|
+
def execute(input)
|
|
35
|
+
goal_id = input["goal_id"]
|
|
36
|
+
goal = @main_session.goals.find_by(id: goal_id)
|
|
37
|
+
return {error: "Goal not found (id: #{goal_id})"} unless goal
|
|
38
|
+
|
|
39
|
+
complete(goal)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Marks the goal as completed. Root goals cascade completion to all
|
|
45
|
+
# active sub-goals within a single transaction so the after_commit
|
|
46
|
+
# broadcast includes the fully cascaded state.
|
|
47
|
+
#
|
|
48
|
+
# Returns an error for already-completed goals so the analytical
|
|
49
|
+
# brain learns to check status before retrying.
|
|
50
|
+
def complete(goal)
|
|
51
|
+
id = goal.id
|
|
52
|
+
return {error: "Goal already completed: #{goal.description} (id: #{id})"} if goal.completed?
|
|
53
|
+
|
|
54
|
+
Goal.transaction do
|
|
55
|
+
goal.update!(status: "completed", completed_at: Time.current)
|
|
56
|
+
goal.cascade_completion! if goal.root?
|
|
57
|
+
end
|
|
58
|
+
"Goal completed: #{goal.description} (id: #{id})"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Reads and activates a workflow on the main session.
|
|
6
|
+
# Returns the full workflow content so the brain can create goals from it.
|
|
7
|
+
# Also sets the workflow as active on the session, injecting its content
|
|
8
|
+
# into the main agent's "Your Expertise" section.
|
|
9
|
+
class ReadWorkflow < ::Tools::Base
|
|
10
|
+
def self.tool_name = "read_workflow"
|
|
11
|
+
|
|
12
|
+
def self.description = "Read a workflow's full content and activate it on the session. " \
|
|
13
|
+
"Use the content to create appropriate goals with set_goal."
|
|
14
|
+
|
|
15
|
+
def self.input_schema
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
name: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Name of the workflow to read (from the available workflows list)"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
required: %w[name]
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param main_session [Session] the session to activate the workflow on
|
|
29
|
+
def initialize(main_session:, **)
|
|
30
|
+
@main_session = main_session
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param input [Hash<String, Object>] with "name" key
|
|
34
|
+
# @return [String] workflow name, description, and full content
|
|
35
|
+
# @return [Hash] with :error key on validation failure
|
|
36
|
+
def execute(input)
|
|
37
|
+
workflow_name = input["name"].to_s.strip
|
|
38
|
+
return {error: "Workflow name cannot be blank"} if workflow_name.empty?
|
|
39
|
+
|
|
40
|
+
workflow = @main_session.activate_workflow(workflow_name)
|
|
41
|
+
format_workflow(workflow)
|
|
42
|
+
rescue Workflows::InvalidDefinitionError => error
|
|
43
|
+
{error: error.message}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def format_workflow(workflow)
|
|
49
|
+
<<~CONTENT
|
|
50
|
+
Workflow: #{workflow.name}
|
|
51
|
+
Description: #{workflow.description}
|
|
52
|
+
|
|
53
|
+
#{workflow.content}
|
|
54
|
+
CONTENT
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Renames the main session with an emoji and short descriptive name.
|
|
6
|
+
# Operates on the main session passed through the registry context,
|
|
7
|
+
# not on the phantom analytical brain session.
|
|
8
|
+
#
|
|
9
|
+
# The analytical brain calls this when a conversation's topic becomes
|
|
10
|
+
# clear or shifts significantly enough to warrant a new name.
|
|
11
|
+
class RenameSession < ::Tools::Base
|
|
12
|
+
def self.tool_name = "rename_session"
|
|
13
|
+
|
|
14
|
+
def self.description = "Rename the conversation session. " \
|
|
15
|
+
"Use one emoji followed by 1-3 descriptive words."
|
|
16
|
+
|
|
17
|
+
def self.input_schema
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
emoji: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "A single emoji representing the conversation topic"
|
|
24
|
+
},
|
|
25
|
+
name: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "1-3 word descriptive name for the session"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: %w[emoji name]
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param main_session [Session] the session to rename
|
|
35
|
+
def initialize(main_session:, **)
|
|
36
|
+
@main_session = main_session
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param input [Hash<String, Object>] with "emoji" and "name" keys
|
|
40
|
+
# @return [String] confirmation message
|
|
41
|
+
# @return [Hash] with :error key on validation failure
|
|
42
|
+
def execute(input)
|
|
43
|
+
error = validate(input)
|
|
44
|
+
return error if error
|
|
45
|
+
|
|
46
|
+
full_name = build_name(input)
|
|
47
|
+
@main_session.update!(name: full_name)
|
|
48
|
+
"Session renamed to: #{full_name}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate(input)
|
|
54
|
+
return {error: "Emoji cannot be blank"} if input["emoji"].to_s.strip.empty?
|
|
55
|
+
{error: "Name cannot be blank"} if input["name"].to_s.strip.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_name(input)
|
|
59
|
+
"#{input["emoji"].to_s.strip} #{input["name"].to_s.strip}".truncate(255)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Creates a goal on the main session. Root goals represent high-level
|
|
6
|
+
# objectives (semantic episodes); sub-goals are TODO-style steps within
|
|
7
|
+
# a root goal. The two-level hierarchy is enforced by the Goal model.
|
|
8
|
+
class SetGoal < ::Tools::Base
|
|
9
|
+
def self.tool_name = "set_goal"
|
|
10
|
+
|
|
11
|
+
def self.description = "Create a goal on the main session. " \
|
|
12
|
+
"Omit parent_goal_id for a root goal, or provide it to create a sub-goal (TODO item)."
|
|
13
|
+
|
|
14
|
+
def self.input_schema
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
description: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "What needs to be accomplished (1-2 sentences)"
|
|
21
|
+
},
|
|
22
|
+
parent_goal_id: {
|
|
23
|
+
type: "integer",
|
|
24
|
+
description: "ID of the parent goal (omit for root goals)"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: %w[description]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param main_session [Session] the session to create the goal on
|
|
32
|
+
def initialize(main_session:, **)
|
|
33
|
+
@main_session = main_session
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param input [Hash<String, Object>] with "description" and optional "parent_goal_id"
|
|
37
|
+
# @return [String] confirmation with goal ID
|
|
38
|
+
# @return [Hash] with :error key on validation failure
|
|
39
|
+
def execute(input)
|
|
40
|
+
description = input["description"].to_s.strip
|
|
41
|
+
return {error: "Description cannot be blank"} if description.empty?
|
|
42
|
+
|
|
43
|
+
goal = @main_session.goals.create!(
|
|
44
|
+
description: description,
|
|
45
|
+
parent_goal_id: input["parent_goal_id"]
|
|
46
|
+
)
|
|
47
|
+
format_confirmation(goal)
|
|
48
|
+
rescue ActiveRecord::RecordInvalid => error
|
|
49
|
+
{error: error.record.errors.full_messages.join(", ")}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def format_confirmation(goal)
|
|
55
|
+
prefix = goal.parent_goal_id ? "Sub-goal" : "Goal"
|
|
56
|
+
"#{prefix} created: #{goal.description} (id: #{goal.id})"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Updates a goal's description on the main session.
|
|
6
|
+
#
|
|
7
|
+
# The analytical brain creates goals early when intent is vague, then
|
|
8
|
+
# refines them as the conversation clarifies scope — e.g. "implement auth"
|
|
9
|
+
# becomes "implement OAuth2 middleware for API endpoints". Without this
|
|
10
|
+
# tool the brain would have to choose between keeping a stale description
|
|
11
|
+
# or creating a duplicate goal.
|
|
12
|
+
#
|
|
13
|
+
# Completed goals cannot be updated; attempting to do so returns an error
|
|
14
|
+
# so the brain learns to check status before calling this tool.
|
|
15
|
+
class UpdateGoal < ::Tools::Base
|
|
16
|
+
def self.tool_name = "update_goal"
|
|
17
|
+
|
|
18
|
+
def self.description = "Update a goal's description. " \
|
|
19
|
+
"Use this to refine a goal as understanding evolves."
|
|
20
|
+
|
|
21
|
+
def self.input_schema
|
|
22
|
+
{
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
goal_id: {
|
|
26
|
+
type: "integer",
|
|
27
|
+
description: "ID of the goal to update"
|
|
28
|
+
},
|
|
29
|
+
description: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "New description for the goal (1-2 sentences)"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: %w[goal_id description]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param main_session [Session] the session owning the goal
|
|
39
|
+
def initialize(main_session:, **)
|
|
40
|
+
@main_session = main_session
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param input [Hash<String, Object>] with "goal_id" and "description"
|
|
44
|
+
# @return [String] confirmation message
|
|
45
|
+
# @return [Hash] with :error key on failure
|
|
46
|
+
def execute(input)
|
|
47
|
+
goal_id = input["goal_id"]
|
|
48
|
+
description = input["description"].to_s.strip
|
|
49
|
+
return {error: "Description cannot be blank"} if description.empty?
|
|
50
|
+
|
|
51
|
+
goal = @main_session.goals.find_by(id: goal_id)
|
|
52
|
+
return {error: "Goal not found (id: #{goal_id})"} unless goal
|
|
53
|
+
return {error: "Cannot update completed goal: #{goal.description} (id: #{goal_id})"} if goal.completed?
|
|
54
|
+
|
|
55
|
+
goal.update!(description: description)
|
|
56
|
+
"Goal updated: #{description} (id: #{goal_id})"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
# Dev-only logger that writes to log/analytical_brain.log.
|
|
5
|
+
# In non-development environments returns a null logger so
|
|
6
|
+
# call sites don't need conditionals.
|
|
7
|
+
#
|
|
8
|
+
# @return [Logger]
|
|
9
|
+
def self.logger
|
|
10
|
+
@logger ||= build_logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build_logger
|
|
14
|
+
return Logger.new(File::NULL) unless Rails.env.development?
|
|
15
|
+
|
|
16
|
+
Logger.new(Rails.root.join("log", "analytical_brain.log")).tap do |log|
|
|
17
|
+
log.formatter = proc { |severity, time, _progname, msg|
|
|
18
|
+
"[#{time.strftime("%H:%M:%S.%L")}] #{severity} #{msg}\n"
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
private_class_method :build_logger
|
|
23
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Anima
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
class Mcp < Thor
|
|
8
|
+
# CLI commands for managing MCP secrets stored in Rails encrypted
|
|
9
|
+
# credentials. Secrets are referenced in mcp.toml via
|
|
10
|
+
# +${credential:key_name}+ syntax.
|
|
11
|
+
#
|
|
12
|
+
# @example Store a secret
|
|
13
|
+
# anima mcp secrets set linear_api_key=sk-xxx
|
|
14
|
+
#
|
|
15
|
+
# @example List stored secret names
|
|
16
|
+
# anima mcp secrets list
|
|
17
|
+
#
|
|
18
|
+
# @example Remove a secret
|
|
19
|
+
# anima mcp secrets remove linear_api_key
|
|
20
|
+
class Secrets < Thor
|
|
21
|
+
def self.exit_on_failure?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "set KEY=VALUE", "Store an MCP secret in encrypted credentials"
|
|
26
|
+
def set(pair)
|
|
27
|
+
key, value = pair.split("=", 2)
|
|
28
|
+
unless value
|
|
29
|
+
say "Error: expected KEY=VALUE format, got '#{pair}'", :red
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_mcp_secrets.set(key, value)
|
|
34
|
+
say "Stored secret '#{key}'.", :green
|
|
35
|
+
rescue ArgumentError => argument_error
|
|
36
|
+
say "Error: #{argument_error.message}", :red
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "list", "List stored MCP secret names (not values)"
|
|
41
|
+
def list
|
|
42
|
+
keys = require_mcp_secrets.list
|
|
43
|
+
|
|
44
|
+
if keys.empty?
|
|
45
|
+
say "No MCP secrets stored.", :yellow
|
|
46
|
+
say "Add one with: anima mcp secrets set KEY=VALUE"
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
keys.each { |key| say " #{key}" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "remove KEY", "Remove an MCP secret from encrypted credentials"
|
|
54
|
+
def remove(key)
|
|
55
|
+
secrets = require_mcp_secrets
|
|
56
|
+
unless secrets.list.include?(key)
|
|
57
|
+
say "Error: secret '#{key}' not found", :red
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
secrets.remove(key)
|
|
62
|
+
say "Removed secret '#{key}'.", :green
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def require_mcp_secrets
|
|
68
|
+
Anima.boot_rails!
|
|
69
|
+
require_relative "../../../mcp/secrets"
|
|
70
|
+
require_relative "../../../credential_store"
|
|
71
|
+
::Mcp::Secrets
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "mcp/secrets"
|
|
5
|
+
|
|
6
|
+
module Anima
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
# CLI commands for managing MCP server configuration in
|
|
9
|
+
# +~/.anima/mcp.toml+. Mirrors the UX of +claude mcp+ commands.
|
|
10
|
+
class Mcp < Thor
|
|
11
|
+
def self.exit_on_failure?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "secrets SUBCOMMAND", "Manage MCP secrets in encrypted credentials"
|
|
16
|
+
subcommand "secrets", Secrets
|
|
17
|
+
|
|
18
|
+
desc "list", "List configured MCP servers with health status"
|
|
19
|
+
def list
|
|
20
|
+
config = build_config
|
|
21
|
+
raw_servers = config.all_servers
|
|
22
|
+
|
|
23
|
+
if raw_servers.empty?
|
|
24
|
+
say "No MCP servers configured.", :yellow
|
|
25
|
+
say "Add one with: anima mcp add <name> <url>"
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
interpolated = interpolated_lookup(config)
|
|
30
|
+
raw_servers.each { |server| display_server(server, interpolated[server["name"]]) }
|
|
31
|
+
config.warnings.each { |warning| say " warning: #{warning}", :yellow }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc "add NAME URL_OR_COMMAND", "Add an MCP server"
|
|
35
|
+
long_desc <<~DESC
|
|
36
|
+
Add an MCP server to ~/.anima/mcp.toml.
|
|
37
|
+
|
|
38
|
+
HTTP server: anima mcp add <name> <url>
|
|
39
|
+
Stdio server: anima mcp add <name> -- <command> [args...]
|
|
40
|
+
|
|
41
|
+
Use -e KEY=VALUE to set environment variables (stdio servers).
|
|
42
|
+
Use -H "Header: Value" to set HTTP headers (HTTP servers).
|
|
43
|
+
Use -s KEY=VALUE to store a secret in encrypted credentials.
|
|
44
|
+
DESC
|
|
45
|
+
option :env, aliases: "-e", type: :string, repeatable: true, banner: "KEY=VALUE",
|
|
46
|
+
desc: "Environment variables (repeatable)"
|
|
47
|
+
option :header, aliases: "-H", type: :string, repeatable: true, banner: "Header: Value",
|
|
48
|
+
desc: "HTTP headers (repeatable)"
|
|
49
|
+
option :secret, aliases: "-s", type: :string, repeatable: true, banner: "KEY=VALUE",
|
|
50
|
+
desc: "Store secret in encrypted credentials (repeatable)"
|
|
51
|
+
def add(name, *rest)
|
|
52
|
+
if rest.empty?
|
|
53
|
+
say "Error: missing server URL or command.", :red
|
|
54
|
+
say ""
|
|
55
|
+
say "Usage:"
|
|
56
|
+
say " anima mcp add <name> <url> # HTTP server"
|
|
57
|
+
say " anima mcp add <name> -- <command> [args...] # stdio server"
|
|
58
|
+
abort_command
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
store_secrets(options[:secret])
|
|
62
|
+
settings = build_settings(rest)
|
|
63
|
+
build_config.add_server(name, settings)
|
|
64
|
+
say "Added #{settings["transport"]} server '#{name}' (#{settings_target(settings)}).", :green
|
|
65
|
+
rescue ArgumentError => argument_error
|
|
66
|
+
say "Error: #{argument_error.message}", :red
|
|
67
|
+
abort_command
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc "remove NAME", "Remove an MCP server"
|
|
71
|
+
def remove(name)
|
|
72
|
+
build_config.remove_server(name)
|
|
73
|
+
say "Removed server '#{name}'.", :green
|
|
74
|
+
rescue ArgumentError => argument_error
|
|
75
|
+
say "Error: #{argument_error.message}", :red
|
|
76
|
+
abort_command
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def abort_command
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_config
|
|
86
|
+
require_relative "../../mcp/config"
|
|
87
|
+
::Mcp::Config.new
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Stores secrets from -s KEY=VALUE flags in encrypted credentials.
|
|
91
|
+
def store_secrets(secret_strings)
|
|
92
|
+
return unless secret_strings&.any?
|
|
93
|
+
|
|
94
|
+
pairs = parse_key_values(secret_strings, label: "secret")
|
|
95
|
+
Anima.boot_rails!
|
|
96
|
+
require_relative "../../mcp/secrets"
|
|
97
|
+
require_relative "../../credential_store"
|
|
98
|
+
pairs.each { |key, value| ::Mcp::Secrets.set(key, value) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Builds interpolated server lookup keyed by name for health checks.
|
|
102
|
+
def interpolated_lookup(config)
|
|
103
|
+
lookup = {}
|
|
104
|
+
populate_lookup(lookup, config.http_servers, "http")
|
|
105
|
+
populate_lookup(lookup, config.stdio_servers, "stdio")
|
|
106
|
+
lookup
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def populate_lookup(lookup, servers, transport)
|
|
110
|
+
servers.each { |server| lookup[server[:name]] = server.merge(transport: transport) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Detects transport from arguments:
|
|
114
|
+
# - First arg starts with http(s):// → HTTP server
|
|
115
|
+
# - Otherwise → stdio server (command + args)
|
|
116
|
+
def build_settings(args)
|
|
117
|
+
first_arg = args.first
|
|
118
|
+
if first_arg.match?(%r{\Ahttps?://})
|
|
119
|
+
build_http_settings(first_arg)
|
|
120
|
+
else
|
|
121
|
+
build_stdio_settings(args)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_http_settings(url)
|
|
126
|
+
settings = {"transport" => "http", "url" => url}
|
|
127
|
+
headers = options[:header]
|
|
128
|
+
settings["headers"] = parse_headers(headers) if headers&.any?
|
|
129
|
+
settings
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_stdio_settings(args)
|
|
133
|
+
command, *remaining_args = args
|
|
134
|
+
settings = {"transport" => "stdio", "command" => command}
|
|
135
|
+
settings["args"] = remaining_args if remaining_args.any?
|
|
136
|
+
env_vars = options[:env]
|
|
137
|
+
settings["env"] = parse_key_values(env_vars) if env_vars&.any?
|
|
138
|
+
settings
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def settings_target(settings)
|
|
142
|
+
if settings["transport"] == "http"
|
|
143
|
+
settings["url"]
|
|
144
|
+
else
|
|
145
|
+
[settings["command"], *settings["args"]].join(" ")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_headers(header_strings)
|
|
150
|
+
header_strings.to_h do |header|
|
|
151
|
+
key, value = header.split(": ", 2)
|
|
152
|
+
raise ArgumentError, "invalid header format '#{header}' — expected 'Name: Value'" unless value
|
|
153
|
+
|
|
154
|
+
[key, value]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_key_values(kv_strings, label: "env var")
|
|
159
|
+
kv_strings.to_h do |kv|
|
|
160
|
+
key, value = kv.split("=", 2)
|
|
161
|
+
raise ArgumentError, "invalid #{label} format '#{kv}' — expected KEY=VALUE" unless value
|
|
162
|
+
|
|
163
|
+
[key, value]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def display_server(raw, interpolated)
|
|
168
|
+
name = raw["name"]
|
|
169
|
+
transport = raw["transport"]
|
|
170
|
+
detail = server_detail(raw, transport)
|
|
171
|
+
status = interpolated ? check_health(interpolated) : set_color("config error", :red)
|
|
172
|
+
|
|
173
|
+
say " #{name}: #{detail} (#{transport}) — #{status}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def server_detail(raw, transport)
|
|
177
|
+
case transport
|
|
178
|
+
when "http" then raw["url"]
|
|
179
|
+
when "stdio" then [raw["command"], *raw["args"]].compact.join(" ")
|
|
180
|
+
else "unknown transport '#{transport}'"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def check_health(server)
|
|
185
|
+
require_relative "../../mcp/health_check"
|
|
186
|
+
result = ::Mcp::HealthCheck.call(server)
|
|
187
|
+
|
|
188
|
+
case result[:status]
|
|
189
|
+
when :connected
|
|
190
|
+
set_color("connected (#{result[:tools]} tools)", :green)
|
|
191
|
+
when :failed
|
|
192
|
+
set_color("failed: #{result[:error]}", :red)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/anima/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "../anima"
|
|
5
|
+
require_relative "cli/mcp"
|
|
5
6
|
|
|
6
7
|
module Anima
|
|
7
8
|
class CLI < Thor
|
|
@@ -47,17 +48,13 @@ module Anima
|
|
|
47
48
|
option :host, desc: "Brain server address (default: #{DEFAULT_HOST})"
|
|
48
49
|
def tui
|
|
49
50
|
require "ratatui_ruby"
|
|
50
|
-
require "net/http"
|
|
51
|
-
require "json"
|
|
52
51
|
require_relative "../tui/app"
|
|
53
52
|
|
|
54
53
|
host = options[:host] || DEFAULT_HOST
|
|
55
54
|
|
|
56
55
|
say "Connecting to brain at #{host}...", :cyan
|
|
57
|
-
session_id = fetch_current_session_with_retry(host)
|
|
58
|
-
say "Session ##{session_id} — starting TUI", :cyan
|
|
59
56
|
|
|
60
|
-
cable_client = TUI::CableClient.new(host: host
|
|
57
|
+
cable_client = TUI::CableClient.new(host: host)
|
|
61
58
|
cable_client.connect
|
|
62
59
|
|
|
63
60
|
TUI::App.new(cable_client: cable_client).run
|
|
@@ -70,41 +67,9 @@ module Anima
|
|
|
70
67
|
say "anima #{Anima::VERSION}"
|
|
71
68
|
end
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
MAX_SESSION_FETCH_ATTEMPTS = 10
|
|
76
|
-
SESSION_FETCH_DELAY = 2 # seconds between retries
|
|
77
|
-
|
|
78
|
-
# Fetches the current session ID from the brain's REST API.
|
|
79
|
-
# Retries up to {MAX_SESSION_FETCH_ATTEMPTS} times if the brain is not running.
|
|
80
|
-
#
|
|
81
|
-
# @param host [String] brain server address
|
|
82
|
-
# @return [Integer] session ID
|
|
83
|
-
def fetch_current_session_with_retry(host)
|
|
84
|
-
attempts = 0
|
|
85
|
-
begin
|
|
86
|
-
fetch_current_session(host)
|
|
87
|
-
rescue Errno::ECONNREFUSED, Net::ReadTimeout, Net::OpenTimeout, SocketError => error
|
|
88
|
-
attempts += 1
|
|
89
|
-
if attempts >= MAX_SESSION_FETCH_ATTEMPTS
|
|
90
|
-
say "Cannot connect to brain after #{MAX_SESSION_FETCH_ATTEMPTS} attempts", :red
|
|
91
|
-
exit 1
|
|
92
|
-
end
|
|
93
|
-
say "Brain not available (#{error.class.name.split("::").last}). " \
|
|
94
|
-
"Retrying #{attempts}/#{MAX_SESSION_FETCH_ATTEMPTS}... (Ctrl+C to cancel)", :yellow
|
|
95
|
-
sleep SESSION_FETCH_DELAY
|
|
96
|
-
retry
|
|
97
|
-
end
|
|
98
|
-
end
|
|
70
|
+
desc "mcp SUBCOMMAND", "Manage MCP server configuration"
|
|
71
|
+
subcommand "mcp", Mcp
|
|
99
72
|
|
|
100
|
-
|
|
101
|
-
# @param host [String] brain server address
|
|
102
|
-
# @return [Integer] session ID
|
|
103
|
-
# @raise [RuntimeError] if the brain returns an error response
|
|
104
|
-
def fetch_current_session(host)
|
|
105
|
-
uri = URI("http://#{host}/api/sessions/current")
|
|
106
|
-
body = Net::HTTP.get(uri)
|
|
107
|
-
JSON.parse(body)["id"]
|
|
108
|
-
end
|
|
73
|
+
private
|
|
109
74
|
end
|
|
110
75
|
end
|