anima-core 0.3.0 → 1.0.1
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 +4 -1
- 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 +182 -6
- 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 -2
- data/.mise.toml +0 -2
data/lib/mcp/secrets.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mcp
|
|
4
|
+
# CRUD operations for MCP server secrets stored in Rails encrypted credentials.
|
|
5
|
+
# Secrets live under the +mcp+ namespace in the credentials file:
|
|
6
|
+
#
|
|
7
|
+
# mcp:
|
|
8
|
+
# linear_api_key: "sk-xxx"
|
|
9
|
+
# mythonix_api_key: "Bearer tok-yyy"
|
|
10
|
+
#
|
|
11
|
+
# Referenced in mcp.toml via +${credential:key_name}+ syntax, resolved at
|
|
12
|
+
# runtime by {Mcp::Config#interpolate_credentials}.
|
|
13
|
+
#
|
|
14
|
+
# @example Storing a secret
|
|
15
|
+
# Mcp::Secrets.set("linear_api_key", "sk-xxx")
|
|
16
|
+
#
|
|
17
|
+
# @example Retrieving a secret
|
|
18
|
+
# Mcp::Secrets.get("linear_api_key") #=> "sk-xxx"
|
|
19
|
+
class Secrets
|
|
20
|
+
NAMESPACE = "mcp"
|
|
21
|
+
|
|
22
|
+
# Keys must be interpolatable via ${credential:key_name} in mcp.toml.
|
|
23
|
+
VALID_KEY_PATTERN = /\A\w+\z/
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Stores a secret in encrypted credentials.
|
|
27
|
+
#
|
|
28
|
+
# @param key [String] secret identifier (e.g. "linear_api_key")
|
|
29
|
+
# @param value [String] secret value
|
|
30
|
+
# @return [void]
|
|
31
|
+
# @raise [ArgumentError] if key contains characters that cannot be
|
|
32
|
+
# referenced via +${credential:key_name}+ syntax
|
|
33
|
+
def set(key, value)
|
|
34
|
+
validate_key!(key)
|
|
35
|
+
CredentialStore.write(NAMESPACE, key => value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Retrieves a secret from encrypted credentials.
|
|
39
|
+
#
|
|
40
|
+
# @param key [String] secret identifier
|
|
41
|
+
# @return [String, nil] secret value or nil if not found
|
|
42
|
+
def get(key)
|
|
43
|
+
CredentialStore.read(NAMESPACE, key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Lists all stored MCP secret keys (not values).
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<String>] secret names
|
|
49
|
+
def list
|
|
50
|
+
CredentialStore.list(NAMESPACE)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Removes a secret from encrypted credentials.
|
|
54
|
+
#
|
|
55
|
+
# @param key [String] secret identifier to remove
|
|
56
|
+
# @return [void]
|
|
57
|
+
def remove(key)
|
|
58
|
+
CredentialStore.remove(NAMESPACE, key)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @raise [ArgumentError] if key is not interpolatable
|
|
64
|
+
def validate_key!(key)
|
|
65
|
+
return if key.match?(VALID_KEY_PATTERN)
|
|
66
|
+
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"invalid secret key '#{key}' — use only letters, numbers, and underscores " \
|
|
69
|
+
"(must match ${credential:key_name} syntax)"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mcp"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module Mcp
|
|
9
|
+
# Client-side stdio transport for MCP servers that communicate via
|
|
10
|
+
# JSON-RPC over stdin/stdout. Conforms to the MCP SDK transport contract
|
|
11
|
+
# (+send_request(request:)+ → Hash) so it plugs into {MCP::Client}
|
|
12
|
+
# identically to the built-in HTTP transport.
|
|
13
|
+
#
|
|
14
|
+
# Spawns the server process lazily on first request. If the process
|
|
15
|
+
# crashes, the next request automatically respawns it. Thread-safe
|
|
16
|
+
# via a mutex around the entire request/response cycle.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# transport = Mcp::StdioTransport.new(command: "linear-toon-mcp")
|
|
20
|
+
# client = MCP::Client.new(transport: transport)
|
|
21
|
+
# client.tools # spawns process, sends tools/list, returns tools
|
|
22
|
+
#
|
|
23
|
+
# @see MCP::Client::HTTP the built-in HTTP transport this mirrors
|
|
24
|
+
class StdioTransport
|
|
25
|
+
# Seconds to wait for graceful SIGTERM shutdown before escalating to SIGKILL.
|
|
26
|
+
GRACEFUL_SHUTDOWN_TIMEOUT = 2
|
|
27
|
+
|
|
28
|
+
# @param command [String] executable to spawn (resolved via $PATH)
|
|
29
|
+
# @param args [Array<String>] command-line arguments for the server process
|
|
30
|
+
# @param env [Hash<String, String>] environment variables merged into
|
|
31
|
+
# the child process's inherited environment
|
|
32
|
+
def initialize(command:, args: [], env: {})
|
|
33
|
+
@command = command
|
|
34
|
+
@args = args
|
|
35
|
+
@env = env
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
@stdin = nil
|
|
38
|
+
@stdout = nil
|
|
39
|
+
@wait_thread = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sends a JSON-RPC request and returns the parsed response.
|
|
43
|
+
# Spawns the server process on first call. If the process died
|
|
44
|
+
# since the last call, respawns automatically.
|
|
45
|
+
#
|
|
46
|
+
# @param request [Hash] complete JSON-RPC request object with
|
|
47
|
+
# +:jsonrpc+, +:id+, +:method+, and optional +:params+ keys
|
|
48
|
+
# @return [Hash] parsed JSON-RPC response (string keys)
|
|
49
|
+
# @raise [MCP::Client::RequestHandlerError] on transport-level errors
|
|
50
|
+
# (process crash, invalid JSON, timeout, command not found)
|
|
51
|
+
def send_request(request:)
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
perform_request(request)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Terminates the server process and releases resources.
|
|
58
|
+
# Safe to call multiple times — subsequent calls are no-ops.
|
|
59
|
+
def shutdown
|
|
60
|
+
@mutex.synchronize { stop_process }
|
|
61
|
+
self.class.unregister(self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- Class-level instance tracking for at_exit cleanup ---
|
|
65
|
+
|
|
66
|
+
@instances = []
|
|
67
|
+
@instances_mutex = Mutex.new
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# @api private
|
|
71
|
+
def register(instance)
|
|
72
|
+
@instances_mutex.synchronize { @instances << instance }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @api private
|
|
76
|
+
def unregister(instance)
|
|
77
|
+
@instances_mutex.synchronize { @instances.delete(instance) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Shuts down all tracked instances. Called automatically via +at_exit+.
|
|
81
|
+
def cleanup_all
|
|
82
|
+
@instances_mutex.synchronize do
|
|
83
|
+
@instances.each { |instance| instance.send(:stop_process) }
|
|
84
|
+
@instances.clear
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
at_exit { Mcp::StdioTransport.cleanup_all }
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def perform_request(request)
|
|
94
|
+
ensure_running
|
|
95
|
+
write_request(request)
|
|
96
|
+
read_response(request)
|
|
97
|
+
rescue Errno::EPIPE, IOError => error
|
|
98
|
+
stop_process
|
|
99
|
+
raise_transport_error("Server process crashed: #{error.message}", request, error)
|
|
100
|
+
rescue JSON::ParserError => error
|
|
101
|
+
stop_process
|
|
102
|
+
raise_transport_error("Invalid JSON from server: #{error.message}", request, error)
|
|
103
|
+
rescue Timeout::Error
|
|
104
|
+
stop_process
|
|
105
|
+
raise_transport_error("No response within #{Anima::Settings.mcp_response_timeout}s", request)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ensure_running
|
|
109
|
+
return if alive?
|
|
110
|
+
|
|
111
|
+
spawn_process
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def alive?
|
|
115
|
+
@wait_thread&.alive? || false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def spawn_process
|
|
119
|
+
@stdin, @stdout, @wait_thread = Open3.popen2(@env, @command, *@args)
|
|
120
|
+
@stdin.set_encoding("UTF-8")
|
|
121
|
+
@stdout.set_encoding("UTF-8")
|
|
122
|
+
self.class.register(self)
|
|
123
|
+
rescue Errno::ENOENT => error
|
|
124
|
+
raise_transport_error("Command not found: #{@command}", {}, error)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write_request(request)
|
|
128
|
+
@stdin.puts(JSON.generate(request))
|
|
129
|
+
@stdin.flush
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Reads lines from stdout until a JSON-RPC response matching the
|
|
133
|
+
# request ID is found. Notifications (messages without a matching id)
|
|
134
|
+
# are silently skipped — the MCP protocol allows servers to emit
|
|
135
|
+
# them at any time.
|
|
136
|
+
def read_response(request)
|
|
137
|
+
request_id = (request[:id] || request["id"]).to_s
|
|
138
|
+
|
|
139
|
+
Timeout.timeout(Anima::Settings.mcp_response_timeout) do
|
|
140
|
+
loop do
|
|
141
|
+
line = @stdout.gets
|
|
142
|
+
raise IOError, "Server process closed stdout" if line.nil?
|
|
143
|
+
|
|
144
|
+
parsed = JSON.parse(line)
|
|
145
|
+
unless parsed.is_a?(Hash)
|
|
146
|
+
raise JSON::ParserError, "Expected JSON object, got #{parsed.class}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return parsed if parsed["id"].to_s == request_id
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def stop_process
|
|
155
|
+
close_pipes
|
|
156
|
+
terminate_process
|
|
157
|
+
@stdin = nil
|
|
158
|
+
@stdout = nil
|
|
159
|
+
@wait_thread = nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def close_pipes
|
|
163
|
+
@stdin&.close rescue IOError # rubocop:disable Style/RescueModifier
|
|
164
|
+
@stdout&.close rescue IOError # rubocop:disable Style/RescueModifier
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Sends SIGTERM and waits up to 2 seconds for the process to exit.
|
|
168
|
+
# Falls back to SIGKILL if the process does not terminate in time.
|
|
169
|
+
def terminate_process
|
|
170
|
+
return unless @wait_thread
|
|
171
|
+
|
|
172
|
+
pid = @wait_thread.pid
|
|
173
|
+
begin
|
|
174
|
+
Process.kill("TERM", pid)
|
|
175
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + GRACEFUL_SHUTDOWN_TIMEOUT
|
|
180
|
+
loop do
|
|
181
|
+
_, status = Process.wait2(pid, Process::WNOHANG)
|
|
182
|
+
break if status
|
|
183
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
184
|
+
Process.kill("KILL", pid) rescue Errno::ESRCH # rubocop:disable Style/RescueModifier
|
|
185
|
+
Process.wait(pid) rescue Errno::ECHILD # rubocop:disable Style/RescueModifier
|
|
186
|
+
break
|
|
187
|
+
end
|
|
188
|
+
sleep 0.05
|
|
189
|
+
end
|
|
190
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
191
|
+
# Already reaped
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def raise_transport_error(message, request, original_error = nil)
|
|
195
|
+
method = request[:method] || request["method"]
|
|
196
|
+
params = request[:params] || request["params"]
|
|
197
|
+
|
|
198
|
+
raise MCP::Client::RequestHandlerError.new(
|
|
199
|
+
message,
|
|
200
|
+
{method: method, params: params},
|
|
201
|
+
error_type: :internal_error,
|
|
202
|
+
original_error: original_error
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/providers/anthropic.rb
CHANGED
|
@@ -7,13 +7,11 @@ module Providers
|
|
|
7
7
|
include HTTParty
|
|
8
8
|
|
|
9
9
|
base_uri "https://api.anthropic.com"
|
|
10
|
-
default_timeout 30
|
|
11
10
|
|
|
12
11
|
TOKEN_PREFIX = "sk-ant-oat01-"
|
|
13
12
|
TOKEN_MIN_LENGTH = 80
|
|
14
13
|
API_VERSION = "2023-06-01"
|
|
15
14
|
REQUIRED_BETA = "oauth-2025-04-20"
|
|
16
|
-
VALIDATION_MODEL = "claude-sonnet-4-20250514"
|
|
17
15
|
|
|
18
16
|
class Error < StandardError; end
|
|
19
17
|
class AuthenticationError < Error; end
|
|
@@ -26,7 +24,7 @@ module Providers
|
|
|
26
24
|
|
|
27
25
|
class << self
|
|
28
26
|
def fetch_token
|
|
29
|
-
token =
|
|
27
|
+
token = CredentialStore.read("anthropic", "subscription_token")
|
|
30
28
|
raise AuthenticationError, <<~MSG.strip if token.blank?
|
|
31
29
|
No Anthropic subscription token found in credentials.
|
|
32
30
|
Use the TUI token setup (Ctrl+a → a) to configure your token.
|
|
@@ -66,7 +64,8 @@ module Providers
|
|
|
66
64
|
response = self.class.post(
|
|
67
65
|
"/v1/messages",
|
|
68
66
|
body: body.to_json,
|
|
69
|
-
headers: request_headers
|
|
67
|
+
headers: request_headers,
|
|
68
|
+
timeout: Anima::Settings.api_timeout
|
|
70
69
|
)
|
|
71
70
|
|
|
72
71
|
handle_response(response)
|
|
@@ -88,7 +87,8 @@ module Providers
|
|
|
88
87
|
response = self.class.post(
|
|
89
88
|
"/v1/messages/count_tokens",
|
|
90
89
|
body: body.to_json,
|
|
91
|
-
headers: request_headers
|
|
90
|
+
headers: request_headers,
|
|
91
|
+
timeout: Anima::Settings.api_timeout
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
result = handle_response(response)
|
|
@@ -101,11 +101,12 @@ module Providers
|
|
|
101
101
|
response = self.class.post(
|
|
102
102
|
"/v1/messages",
|
|
103
103
|
body: {
|
|
104
|
-
model:
|
|
104
|
+
model: Anima::Settings.model,
|
|
105
105
|
messages: [{role: "user", content: "Hi"}],
|
|
106
106
|
max_tokens: 1
|
|
107
107
|
}.to_json,
|
|
108
|
-
headers: request_headers
|
|
108
|
+
headers: request_headers,
|
|
109
|
+
timeout: Anima::Settings.api_timeout
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
case response.code
|
data/lib/shell_session.rb
CHANGED
|
@@ -16,9 +16,6 @@ require "timeout"
|
|
|
16
16
|
# # => {stdout: "/tmp", stderr: "", exit_code: 0}
|
|
17
17
|
# session.finalize
|
|
18
18
|
class ShellSession
|
|
19
|
-
COMMAND_TIMEOUT = 30
|
|
20
|
-
MAX_OUTPUT_BYTES = 100_000
|
|
21
|
-
|
|
22
19
|
# @return [String, nil] current working directory of the shell process
|
|
23
20
|
attr_reader :pwd
|
|
24
21
|
|
|
@@ -140,12 +137,14 @@ class ShellSession
|
|
|
140
137
|
@stderr_buffer = []
|
|
141
138
|
@stderr_bytes = 0
|
|
142
139
|
@stderr_truncated = false
|
|
140
|
+
@max_output_bytes = Anima::Settings.max_output_bytes
|
|
143
141
|
@stderr_thread = Thread.new do
|
|
142
|
+
max_bytes = @max_output_bytes
|
|
144
143
|
File.open(@fifo_path, "r") do |fifo|
|
|
145
144
|
while (line = fifo.gets)
|
|
146
145
|
cleaned = line.chomp.delete("\r")
|
|
147
146
|
@stderr_mutex.synchronize do
|
|
148
|
-
if @stderr_bytes <
|
|
147
|
+
if @stderr_bytes < max_bytes
|
|
149
148
|
@stderr_buffer << cleaned
|
|
150
149
|
@stderr_bytes += cleaned.bytesize
|
|
151
150
|
else
|
|
@@ -172,8 +171,9 @@ class ShellSession
|
|
|
172
171
|
def execute_in_pty(command)
|
|
173
172
|
clear_stderr
|
|
174
173
|
marker = "__ANIMA_#{SecureRandom.hex(8)}__"
|
|
174
|
+
timeout = Anima::Settings.command_timeout
|
|
175
175
|
|
|
176
|
-
Timeout.timeout(
|
|
176
|
+
Timeout.timeout(timeout) do
|
|
177
177
|
# All on one line: run command, capture exit code, ensure newline
|
|
178
178
|
# before marker so output without trailing newline doesn't merge.
|
|
179
179
|
@pty_stdin.puts "#{command}; __anima_ec=$?; echo; echo '#{marker}' $__anima_ec"
|
|
@@ -190,7 +190,7 @@ class ShellSession
|
|
|
190
190
|
end
|
|
191
191
|
rescue Timeout::Error
|
|
192
192
|
recover_from_timeout
|
|
193
|
-
{error: "Command timed out after #{
|
|
193
|
+
{error: "Command timed out after #{timeout} seconds"}
|
|
194
194
|
rescue Errno::EIO
|
|
195
195
|
@alive = false
|
|
196
196
|
{error: "Shell session terminated unexpectedly"}
|
|
@@ -260,7 +260,7 @@ class ShellSession
|
|
|
260
260
|
@stderr_buffer.clear
|
|
261
261
|
@stderr_bytes = 0
|
|
262
262
|
@stderr_truncated = false
|
|
263
|
-
truncated ? result + "\n\n[Truncated: output exceeded #{
|
|
263
|
+
truncated ? result + "\n\n[Truncated: output exceeded #{@max_output_bytes} bytes]" : result
|
|
264
264
|
end
|
|
265
265
|
end
|
|
266
266
|
|
|
@@ -274,12 +274,13 @@ class ShellSession
|
|
|
274
274
|
end
|
|
275
275
|
|
|
276
276
|
def truncate(output)
|
|
277
|
-
|
|
277
|
+
max_bytes = @max_output_bytes
|
|
278
|
+
return output if output.bytesize <= max_bytes
|
|
278
279
|
|
|
279
|
-
output.byteslice(0,
|
|
280
|
+
output.byteslice(0, max_bytes)
|
|
280
281
|
.force_encoding("UTF-8")
|
|
281
282
|
.scrub +
|
|
282
|
-
"\n\n[Truncated: output exceeded #{
|
|
283
|
+
"\n\n[Truncated: output exceeded #{max_bytes} bytes]"
|
|
283
284
|
end
|
|
284
285
|
|
|
285
286
|
def shutdown
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Skills
|
|
6
|
+
class InvalidDefinitionError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# A domain knowledge skill parsed from a Markdown definition file.
|
|
9
|
+
# YAML frontmatter holds metadata; the Markdown body is the knowledge
|
|
10
|
+
# content injected into the main agent's system prompt when active.
|
|
11
|
+
#
|
|
12
|
+
# Skills are passive knowledge — they describe WHAT you know, not
|
|
13
|
+
# WHAT to do. The analytical brain activates/deactivates them based
|
|
14
|
+
# on conversation context.
|
|
15
|
+
#
|
|
16
|
+
# @example Skill file format
|
|
17
|
+
# ---
|
|
18
|
+
# name: gh-issue
|
|
19
|
+
# description: "GitHub issue writing with WHAT/WHY/HOW framework."
|
|
20
|
+
# ---
|
|
21
|
+
#
|
|
22
|
+
# # GitHub Issue Writing
|
|
23
|
+
# Write issues with clear rationale...
|
|
24
|
+
class Definition
|
|
25
|
+
# @return [String] unique skill identifier used in activate_skill(name: "...")
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
# @return [String] description shown to the analytical brain for relevance matching
|
|
29
|
+
attr_reader :description
|
|
30
|
+
|
|
31
|
+
# @return [String] knowledge content (Markdown body) injected into system prompt
|
|
32
|
+
attr_reader :content
|
|
33
|
+
|
|
34
|
+
# @return [String] file path this definition was loaded from
|
|
35
|
+
attr_reader :source_path
|
|
36
|
+
|
|
37
|
+
def initialize(name:, description:, content:, source_path: "")
|
|
38
|
+
@name = name
|
|
39
|
+
@description = description
|
|
40
|
+
@content = content
|
|
41
|
+
@source_path = source_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parses a Markdown file with YAML frontmatter into a Definition.
|
|
45
|
+
#
|
|
46
|
+
# @param path [String, Pathname] path to the .md file
|
|
47
|
+
# @return [Definition]
|
|
48
|
+
# @raise [InvalidDefinitionError] if required fields are missing or frontmatter is malformed
|
|
49
|
+
def self.from_file(path)
|
|
50
|
+
raw = File.read(path)
|
|
51
|
+
frontmatter, body = parse_frontmatter(raw)
|
|
52
|
+
|
|
53
|
+
validate_required_fields!(frontmatter, path)
|
|
54
|
+
|
|
55
|
+
new(
|
|
56
|
+
name: frontmatter["name"].to_s.strip,
|
|
57
|
+
description: frontmatter["description"].to_s.strip,
|
|
58
|
+
content: body.strip,
|
|
59
|
+
source_path: path.to_s
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param raw [String] raw file content with YAML frontmatter
|
|
64
|
+
# @return [Array(Hash, String)] parsed frontmatter and body text
|
|
65
|
+
# @raise [InvalidDefinitionError] if frontmatter is missing or malformed
|
|
66
|
+
def self.parse_frontmatter(raw)
|
|
67
|
+
# Opening "---" must be followed by a newline (not just whitespace).
|
|
68
|
+
# Non-greedy (.*?\n) captures YAML lines up to the closing "---".
|
|
69
|
+
# Closing "---" may optionally be followed by a newline before the body.
|
|
70
|
+
# The /m flag lets (.*) in the body capture across newlines.
|
|
71
|
+
match = raw.match(/\A---\s*\n(.*?\n)---\s*\n?(.*)\z/m)
|
|
72
|
+
raise InvalidDefinitionError, "Missing YAML frontmatter" unless match
|
|
73
|
+
|
|
74
|
+
frontmatter = YAML.safe_load(match[1])
|
|
75
|
+
raise InvalidDefinitionError, "Frontmatter is not a valid YAML mapping" unless frontmatter.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
[frontmatter, match[2]]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
NAME_FORMAT = /\A[a-z0-9][a-z0-9_-]*\z/
|
|
81
|
+
|
|
82
|
+
def self.validate_required_fields!(frontmatter, path)
|
|
83
|
+
%w[name description].each do |field|
|
|
84
|
+
value = frontmatter[field].to_s.strip
|
|
85
|
+
raise InvalidDefinitionError, "Missing required field '#{field}' in #{path}" if value.empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
name = frontmatter["name"].to_s.strip
|
|
89
|
+
unless name.match?(NAME_FORMAT)
|
|
90
|
+
raise InvalidDefinitionError,
|
|
91
|
+
"Invalid skill name '#{name}' in #{path} — must be lowercase alphanumeric with hyphens/underscores"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_class_method :parse_frontmatter, :validate_required_fields!
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skills
|
|
4
|
+
# Loads skill definitions from Markdown files and provides lookup.
|
|
5
|
+
# Supports two formats:
|
|
6
|
+
# - Flat file: skills/skill-name.md
|
|
7
|
+
# - Directory: skills/skill-name/SKILL.md (with optional references/ and examples/)
|
|
8
|
+
# Scans two directories:
|
|
9
|
+
# 1. Built-in skills shipped with Anima (skills/ in the gem root)
|
|
10
|
+
# 2. User-defined skills (~/.anima/skills/)
|
|
11
|
+
# User skills override built-in ones when names collide.
|
|
12
|
+
class Registry
|
|
13
|
+
# @return [Hash{String => Definition}] loaded definitions keyed by name
|
|
14
|
+
attr_reader :skills
|
|
15
|
+
|
|
16
|
+
BUILTIN_DIR = File.expand_path("../../skills", __dir__).freeze
|
|
17
|
+
USER_DIR = File.expand_path("~/.anima/skills").freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@skills = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the global registry, lazily loaded on first access.
|
|
24
|
+
#
|
|
25
|
+
# @return [Registry]
|
|
26
|
+
def self.instance
|
|
27
|
+
@instance ||= new.load_all
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Reloads the global registry from disk.
|
|
31
|
+
#
|
|
32
|
+
# @return [Registry]
|
|
33
|
+
def self.reload!
|
|
34
|
+
@instance = new.load_all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Loads definitions from both built-in and user directories.
|
|
38
|
+
# User definitions override built-in ones with the same name.
|
|
39
|
+
#
|
|
40
|
+
# @return [self]
|
|
41
|
+
def load_all
|
|
42
|
+
load_directory(BUILTIN_DIR)
|
|
43
|
+
load_directory(USER_DIR)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Loads skill definitions from a single directory.
|
|
48
|
+
# Supports flat files (*.md) and directory-based skills (*/SKILL.md).
|
|
49
|
+
#
|
|
50
|
+
# @param dir [String] directory path to scan for skill definitions
|
|
51
|
+
# (flat .md files and SKILL.md inside subdirectories)
|
|
52
|
+
# @return [void]
|
|
53
|
+
def load_directory(dir)
|
|
54
|
+
return unless Dir.exist?(dir)
|
|
55
|
+
|
|
56
|
+
skill_files(dir).each do |path|
|
|
57
|
+
definition = Definition.from_file(path)
|
|
58
|
+
@skills[definition.name] = definition
|
|
59
|
+
rescue InvalidDefinitionError => error
|
|
60
|
+
Rails.logger.warn("Skipping invalid skill definition #{path}: #{error.message}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Looks up a named skill definition.
|
|
65
|
+
#
|
|
66
|
+
# @param name [String] skill name
|
|
67
|
+
# @return [Definition, nil]
|
|
68
|
+
def find(name)
|
|
69
|
+
@skills[name]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Skill names and descriptions for inclusion in the analytical brain's context.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash{String => String}] name => description
|
|
75
|
+
def catalog
|
|
76
|
+
@skills.transform_values(&:description)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Array<String>] registered skill names
|
|
80
|
+
def available_names
|
|
81
|
+
@skills.keys
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def any?
|
|
86
|
+
@skills.any?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
def size
|
|
91
|
+
@skills.size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Finds all skill definition files in a directory — both flat .md files
|
|
97
|
+
# and SKILL.md files inside subdirectories.
|
|
98
|
+
#
|
|
99
|
+
# @param dir [String] directory to scan
|
|
100
|
+
# @return [Array<String>] sorted paths to skill definition files
|
|
101
|
+
def skill_files(dir)
|
|
102
|
+
Dir.glob([File.join(dir, "*.md"), File.join(dir, "*/SKILL.md")]).sort
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/tools/edit.rb
CHANGED
|
@@ -16,8 +16,6 @@ module Tools
|
|
|
16
16
|
# "new_text" => "def greet\n 'hello'\nend")
|
|
17
17
|
# # => "--- app.rb\n+++ app.rb\n@@ -1,3 +1,3 @@\n ..."
|
|
18
18
|
class Edit < Base
|
|
19
|
-
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
20
|
-
|
|
21
19
|
def self.tool_name = "edit"
|
|
22
20
|
|
|
23
21
|
def self.description = "Replace exact text in a file. old_text must match exactly one location; " \
|
|
@@ -79,9 +77,10 @@ module Tools
|
|
|
79
77
|
return {error: "Is a directory: #{path}"} if File.directory?(path)
|
|
80
78
|
return {error: "Permission denied: #{path}"} unless File.readable?(path) && File.writable?(path)
|
|
81
79
|
size = File.size(path)
|
|
82
|
-
|
|
80
|
+
max_size = Anima::Settings.max_file_size
|
|
81
|
+
if size > max_size
|
|
83
82
|
{error: "File is #{size} bytes (#{size / 1_048_576} MB). " \
|
|
84
|
-
"Max editable size is #{
|
|
83
|
+
"Max editable size is #{max_size / 1_048_576} MB. Use bash tool with sed instead."}
|
|
85
84
|
end
|
|
86
85
|
end
|
|
87
86
|
|