swarm_memory 2.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 +7 -0
- data/LICENSE +21 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +349 -0
- data/lib/claude_swarm/claude_mcp_server.rb +77 -0
- data/lib/claude_swarm/cli.rb +712 -0
- data/lib/claude_swarm/commands/ps.rb +216 -0
- data/lib/claude_swarm/commands/show.rb +139 -0
- data/lib/claude_swarm/configuration.rb +363 -0
- data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
- data/lib/claude_swarm/json_handler.rb +91 -0
- data/lib/claude_swarm/mcp_generator.rb +248 -0
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +254 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +879 -0
- data/lib/claude_swarm/process_tracker.rb +78 -0
- data/lib/claude_swarm/session_cost_calculator.rb +209 -0
- data/lib/claude_swarm/session_path.rb +42 -0
- data/lib/claude_swarm/settings_generator.rb +77 -0
- data/lib/claude_swarm/system_utils.rb +46 -0
- data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
- data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
- data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
- data/lib/claude_swarm/tools/task_tool.rb +63 -0
- data/lib/claude_swarm/version.rb +5 -0
- data/lib/claude_swarm/worktree_manager.rb +475 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +69 -0
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +45 -0
- data/lib/swarm_memory/adapters/base.rb +140 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +286 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +40 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +145 -0
- data/lib/swarm_memory/tools/memory_grep.rb +209 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +215 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1144 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +167 -0
- metadata +313 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module Tools
|
|
5
|
+
class ResetSessionTool < FastMcp::Tool
|
|
6
|
+
tool_name "reset_session"
|
|
7
|
+
description "Reset the Claude session for this agent, starting fresh on the next task"
|
|
8
|
+
|
|
9
|
+
arguments do
|
|
10
|
+
# No arguments needed
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
executor = ClaudeMcpServer.executor
|
|
15
|
+
executor.reset_session
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
success: true,
|
|
19
|
+
message: "Session has been reset",
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module Tools
|
|
5
|
+
class SessionInfoTool < FastMcp::Tool
|
|
6
|
+
tool_name "session_info"
|
|
7
|
+
description "Get information about the current Claude session for this agent"
|
|
8
|
+
|
|
9
|
+
arguments do
|
|
10
|
+
# No arguments needed
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
executor = ClaudeMcpServer.executor
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
has_session: executor.has_session?,
|
|
18
|
+
session_id: executor.session_id,
|
|
19
|
+
working_directory: executor.working_directory,
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module Tools
|
|
5
|
+
class TaskTool < FastMcp::Tool
|
|
6
|
+
tool_name "task"
|
|
7
|
+
description "Execute a task using Claude Code. There is no description parameter."
|
|
8
|
+
annotations(read_only_hint: true, open_world_hint: false, destructive_hint: false)
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:prompt).filled(:string).description("The task or question for the agent")
|
|
12
|
+
optional(:new_session).filled(:bool).description("Start a new session (default: false)")
|
|
13
|
+
optional(:system_prompt).filled(:string).description("Override the system prompt for this request")
|
|
14
|
+
optional(:description).filled(:string).description("A description for the request")
|
|
15
|
+
optional(:thinking_budget).filled(:string).description("Thinking budget: \"think\" < \"think hard\" < \"think harder\" < \"ultrathink\". Each level increases Claude's thinking allocation. Auto-select based on task complexity.")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(prompt:, new_session: false, system_prompt: nil, description: nil, thinking_budget: nil)
|
|
19
|
+
executor = ClaudeMcpServer.executor
|
|
20
|
+
instance_config = ClaudeMcpServer.instance_config
|
|
21
|
+
|
|
22
|
+
# Prepend thinking budget to prompt if provided
|
|
23
|
+
final_prompt = if thinking_budget
|
|
24
|
+
"#{thinking_budget}: #{prompt}"
|
|
25
|
+
else
|
|
26
|
+
prompt
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
options = {
|
|
30
|
+
new_session: new_session,
|
|
31
|
+
system_prompt: system_prompt || instance_config[:prompt],
|
|
32
|
+
description: description,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Add allowed tools from instance config
|
|
36
|
+
options[:allowed_tools] = instance_config[:allowed_tools] if instance_config[:allowed_tools]&.any?
|
|
37
|
+
|
|
38
|
+
# Add disallowed tools from instance config
|
|
39
|
+
options[:disallowed_tools] = instance_config[:disallowed_tools] if instance_config[:disallowed_tools]&.any?
|
|
40
|
+
|
|
41
|
+
# Add connections from instance config
|
|
42
|
+
options[:connections] = instance_config[:connections] if instance_config[:connections]&.any?
|
|
43
|
+
|
|
44
|
+
response = executor.execute(final_prompt, options)
|
|
45
|
+
|
|
46
|
+
# Validate the response has a result
|
|
47
|
+
unless response.is_a?(Hash) && response.key?("result")
|
|
48
|
+
raise "Invalid response from executor: missing 'result' field. Response structure: #{response.keys.join(", ")}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result = response["result"]
|
|
52
|
+
|
|
53
|
+
# Validate the result is not empty
|
|
54
|
+
if result.nil? || (result.is_a?(String) && result.strip.empty?)
|
|
55
|
+
raise "Agent #{instance_config[:name]} returned an empty response. The task was executed but no content was provided."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Return just the result text as expected by MCP
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
class WorktreeManager
|
|
5
|
+
include SystemUtils
|
|
6
|
+
|
|
7
|
+
attr_reader :shared_worktree_name, :created_worktrees
|
|
8
|
+
|
|
9
|
+
def initialize(cli_worktree_option = nil, session_id: nil)
|
|
10
|
+
@cli_worktree_option = cli_worktree_option
|
|
11
|
+
@session_id = session_id
|
|
12
|
+
# Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
|
|
13
|
+
@shared_worktree_name = if cli_worktree_option.nil? || cli_worktree_option.empty? || cli_worktree_option == "worktree"
|
|
14
|
+
generate_worktree_name
|
|
15
|
+
else
|
|
16
|
+
cli_worktree_option
|
|
17
|
+
end
|
|
18
|
+
@created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
|
|
19
|
+
@instance_worktree_configs = {} # Stores per-instance worktree settings
|
|
20
|
+
@instance_directories = {} # Stores original resolved directories for each instance
|
|
21
|
+
@instance_worktree_paths = {} # Stores worktree paths for each instance
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def setup_worktrees(instances)
|
|
25
|
+
# First pass: determine worktree configuration for each instance
|
|
26
|
+
instances.each do |instance|
|
|
27
|
+
worktree_config = determine_worktree_config(instance)
|
|
28
|
+
@instance_worktree_configs[instance[:name]] = worktree_config
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Second pass: create necessary worktrees
|
|
32
|
+
worktrees_to_create = collect_worktrees_to_create(instances)
|
|
33
|
+
worktrees_to_create.each do |repo_root, worktree_name|
|
|
34
|
+
create_worktree(repo_root, worktree_name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Third pass: map instance directories to worktree paths
|
|
38
|
+
instances.each do |instance|
|
|
39
|
+
worktree_config = @instance_worktree_configs[instance[:name]]
|
|
40
|
+
|
|
41
|
+
if ENV["CLAUDE_SWARM_DEBUG"]
|
|
42
|
+
puts "Debug [WorktreeManager]: Processing instance #{instance[:name]}"
|
|
43
|
+
puts "Debug [WorktreeManager]: Worktree config: #{worktree_config.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Store original directories (resolved)
|
|
47
|
+
original_dirs = instance[:directories] || [instance[:directory]]
|
|
48
|
+
resolved_dirs = original_dirs.map { |dir| dir ? File.expand_path(dir) : nil }.compact
|
|
49
|
+
@instance_directories[instance[:name]] = resolved_dirs
|
|
50
|
+
|
|
51
|
+
if worktree_config[:skip]
|
|
52
|
+
# No worktree, paths remain the same
|
|
53
|
+
@instance_worktree_paths[instance[:name]] = resolved_dirs
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
worktree_name = worktree_config[:name]
|
|
58
|
+
mapped_dirs = original_dirs.map { |dir| map_to_worktree_path(dir, worktree_name) }
|
|
59
|
+
|
|
60
|
+
# Store the worktree paths
|
|
61
|
+
@instance_worktree_paths[instance[:name]] = mapped_dirs
|
|
62
|
+
|
|
63
|
+
if ENV["CLAUDE_SWARM_DEBUG"]
|
|
64
|
+
puts "Debug [WorktreeManager]: Original dirs: #{original_dirs.inspect}"
|
|
65
|
+
puts "Debug [WorktreeManager]: Mapped dirs: #{mapped_dirs.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if instance[:directories]
|
|
69
|
+
instance[:directories] = mapped_dirs
|
|
70
|
+
# Also update the single directory field for backward compatibility
|
|
71
|
+
instance[:directory] = mapped_dirs.first
|
|
72
|
+
else
|
|
73
|
+
instance[:directory] = mapped_dirs.first
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "Debug [WorktreeManager]: Updated instance[:directory] to: #{instance[:directory]}" if ENV["CLAUDE_SWARM_DEBUG"]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def map_to_worktree_path(original_path, worktree_name)
|
|
81
|
+
return original_path unless original_path
|
|
82
|
+
|
|
83
|
+
expanded_path = File.expand_path(original_path)
|
|
84
|
+
repo_root = find_git_root(expanded_path)
|
|
85
|
+
|
|
86
|
+
if ENV["CLAUDE_SWARM_DEBUG"]
|
|
87
|
+
puts "Debug [map_to_worktree_path]: Original path: #{original_path}"
|
|
88
|
+
puts "Debug [map_to_worktree_path]: Expanded path: #{expanded_path}"
|
|
89
|
+
puts "Debug [map_to_worktree_path]: Repo root: #{repo_root}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return original_path unless repo_root
|
|
93
|
+
|
|
94
|
+
# Check if we have a worktree for this repo and name
|
|
95
|
+
worktree_key = "#{repo_root}:#{worktree_name}"
|
|
96
|
+
worktree_path = @created_worktrees[worktree_key]
|
|
97
|
+
|
|
98
|
+
if ENV["CLAUDE_SWARM_DEBUG"]
|
|
99
|
+
puts "Debug [map_to_worktree_path]: Worktree key: #{worktree_key}"
|
|
100
|
+
puts "Debug [map_to_worktree_path]: Worktree path: #{worktree_path}"
|
|
101
|
+
puts "Debug [map_to_worktree_path]: Created worktrees: #{@created_worktrees.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return original_path unless worktree_path
|
|
105
|
+
|
|
106
|
+
# Calculate relative path from repo root
|
|
107
|
+
relative_path = Pathname.new(expanded_path).relative_path_from(Pathname.new(repo_root)).to_s
|
|
108
|
+
|
|
109
|
+
# Return the equivalent path in the worktree
|
|
110
|
+
result = if relative_path == "."
|
|
111
|
+
worktree_path
|
|
112
|
+
else
|
|
113
|
+
File.join(worktree_path, relative_path)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]
|
|
117
|
+
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cleanup_worktrees
|
|
122
|
+
@created_worktrees.each do |worktree_key, worktree_path|
|
|
123
|
+
repo_root = worktree_key.split(":", 2).first
|
|
124
|
+
next unless File.exist?(worktree_path)
|
|
125
|
+
|
|
126
|
+
# Check for uncommitted changes
|
|
127
|
+
if has_uncommitted_changes?(worktree_path)
|
|
128
|
+
puts "⚠️ Warning: Worktree has uncommitted changes, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check for unpushed commits
|
|
133
|
+
has_unpushed = has_unpushed_commits?(worktree_path)
|
|
134
|
+
|
|
135
|
+
if has_unpushed
|
|
136
|
+
puts "⚠️ Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
puts "Removing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
141
|
+
|
|
142
|
+
# Remove the worktree
|
|
143
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", worktree_path)
|
|
144
|
+
next if status.success?
|
|
145
|
+
|
|
146
|
+
puts "Warning: Failed to remove worktree: #{output}"
|
|
147
|
+
# Try force remove
|
|
148
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
|
|
149
|
+
puts "Force remove result: #{output}" unless status.success?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Clean up external worktree directories
|
|
153
|
+
cleanup_external_directories
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
puts "Error during worktree cleanup: #{e.message}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def cleanup_external_directories
|
|
159
|
+
# Remove session-specific worktree directory if it exists and is empty
|
|
160
|
+
return unless @session_id
|
|
161
|
+
|
|
162
|
+
session_worktree_dir = ClaudeSwarm.joined_worktrees_dir(@session_id)
|
|
163
|
+
return unless File.exist?(session_worktree_dir)
|
|
164
|
+
|
|
165
|
+
# Try to remove the directory tree
|
|
166
|
+
begin
|
|
167
|
+
# Remove all empty directories recursively
|
|
168
|
+
Dir.glob(File.join(session_worktree_dir, "**/*"), File::FNM_DOTMATCH).reverse_each do |path|
|
|
169
|
+
next if path.end_with?("/.", "/..")
|
|
170
|
+
|
|
171
|
+
FileUtils.rmdir(path) if File.directory?(path) && Dir.empty?(path)
|
|
172
|
+
end
|
|
173
|
+
# Finally try to remove the session directory itself
|
|
174
|
+
FileUtils.rmdir(session_worktree_dir) if Dir.empty?(session_worktree_dir)
|
|
175
|
+
rescue Errno::ENOTEMPTY
|
|
176
|
+
puts "Note: Session worktree directory not empty, leaving in place: #{session_worktree_dir}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
puts "Warning: Error cleaning up worktree directories: #{e.message}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def session_metadata
|
|
183
|
+
# Build instance details with resolved paths and worktree mappings
|
|
184
|
+
instance_details = {}
|
|
185
|
+
|
|
186
|
+
@instance_worktree_configs.each do |instance_name, worktree_config|
|
|
187
|
+
instance_details[instance_name] = {
|
|
188
|
+
worktree_config: worktree_config,
|
|
189
|
+
directories: @instance_directories[instance_name] || {},
|
|
190
|
+
worktree_paths: @instance_worktree_paths[instance_name] || {},
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
enabled: true,
|
|
196
|
+
shared_name: @shared_worktree_name,
|
|
197
|
+
created_paths: @created_worktrees.dup,
|
|
198
|
+
instance_configs: instance_details,
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Deprecated method for backward compatibility
|
|
203
|
+
def worktree_name
|
|
204
|
+
@shared_worktree_name
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def external_worktree_path(repo_root, worktree_name)
|
|
210
|
+
# Get repository name from path
|
|
211
|
+
repo_name = sanitize_repo_name(File.basename(repo_root))
|
|
212
|
+
|
|
213
|
+
# Add a short hash of the full path to handle multiple repos with same name
|
|
214
|
+
path_hash = Digest::SHA256.hexdigest(repo_root)[0..7]
|
|
215
|
+
unique_repo_name = "#{repo_name}-#{path_hash}"
|
|
216
|
+
|
|
217
|
+
# Build external path: ~/.claude-swarm/worktrees/[session_id]/[repo_name-hash]/[worktree_name]
|
|
218
|
+
base_dir = ClaudeSwarm.joined_worktrees_dir
|
|
219
|
+
|
|
220
|
+
# Validate base directory is accessible
|
|
221
|
+
begin
|
|
222
|
+
FileUtils.mkdir_p(base_dir)
|
|
223
|
+
rescue Errno::EACCES
|
|
224
|
+
# Fall back to temp directory if home is not writable
|
|
225
|
+
base_dir = File.join(Dir.tmpdir, ".claude-swarm", "worktrees")
|
|
226
|
+
FileUtils.mkdir_p(base_dir)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
session_dir = @session_id || "default"
|
|
230
|
+
File.join(base_dir, session_dir, unique_repo_name, worktree_name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def sanitize_repo_name(name)
|
|
234
|
+
# Replace problematic characters with underscores
|
|
235
|
+
name.gsub(/[^a-zA-Z0-9._-]/, "_")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def generate_worktree_name
|
|
239
|
+
# Use session ID if available, otherwise generate a random suffix
|
|
240
|
+
if @session_id
|
|
241
|
+
"worktree-#{@session_id}"
|
|
242
|
+
else
|
|
243
|
+
# Fallback to random suffix for tests or when session ID is not available
|
|
244
|
+
random_suffix = SecureRandom.alphanumeric(5).downcase
|
|
245
|
+
"worktree-#{random_suffix}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def determine_worktree_config(instance)
|
|
250
|
+
# Check instance-level worktree setting
|
|
251
|
+
instance_worktree = instance[:worktree]
|
|
252
|
+
|
|
253
|
+
if instance_worktree.nil?
|
|
254
|
+
# No instance-level setting, follow CLI behavior
|
|
255
|
+
if @cli_worktree_option.nil?
|
|
256
|
+
{ skip: true }
|
|
257
|
+
else
|
|
258
|
+
{ skip: false, name: @shared_worktree_name }
|
|
259
|
+
end
|
|
260
|
+
elsif instance_worktree == false
|
|
261
|
+
# Explicitly disabled for this instance
|
|
262
|
+
{ skip: true }
|
|
263
|
+
elsif instance_worktree == true
|
|
264
|
+
# Use shared worktree (either from CLI or auto-generated)
|
|
265
|
+
{ skip: false, name: @shared_worktree_name }
|
|
266
|
+
elsif instance_worktree.is_a?(String)
|
|
267
|
+
# Use custom worktree name
|
|
268
|
+
{ skip: false, name: instance_worktree }
|
|
269
|
+
else
|
|
270
|
+
raise Error, "Invalid worktree configuration for instance '#{instance[:name]}': #{instance_worktree.inspect}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def collect_worktrees_to_create(instances)
|
|
275
|
+
worktrees_needed = {}
|
|
276
|
+
|
|
277
|
+
instances.each do |instance|
|
|
278
|
+
worktree_config = @instance_worktree_configs[instance[:name]]
|
|
279
|
+
next if worktree_config[:skip]
|
|
280
|
+
|
|
281
|
+
worktree_name = worktree_config[:name]
|
|
282
|
+
directories = instance[:directories] || [instance[:directory]]
|
|
283
|
+
|
|
284
|
+
directories.each do |dir|
|
|
285
|
+
next unless dir
|
|
286
|
+
|
|
287
|
+
expanded_dir = File.expand_path(dir)
|
|
288
|
+
repo_root = find_git_root(expanded_dir)
|
|
289
|
+
next unless repo_root
|
|
290
|
+
|
|
291
|
+
# Track unique repo_root:worktree_name combinations
|
|
292
|
+
worktrees_needed[repo_root] ||= Set.new
|
|
293
|
+
worktrees_needed[repo_root].add(worktree_name)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Convert to array of [repo_root, worktree_name] pairs
|
|
298
|
+
result = []
|
|
299
|
+
worktrees_needed.each do |repo_root, worktree_names|
|
|
300
|
+
worktree_names.each do |worktree_name|
|
|
301
|
+
result << [repo_root, worktree_name]
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
result
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def find_git_root(path)
|
|
308
|
+
current = File.expand_path(path)
|
|
309
|
+
|
|
310
|
+
while current != "/"
|
|
311
|
+
return current if File.exist?(File.join(current, ".git"))
|
|
312
|
+
|
|
313
|
+
current = File.dirname(current)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def create_worktree(repo_root, worktree_name)
|
|
320
|
+
worktree_key = "#{repo_root}:#{worktree_name}"
|
|
321
|
+
# Create worktrees in external directory
|
|
322
|
+
worktree_path = external_worktree_path(repo_root, worktree_name)
|
|
323
|
+
|
|
324
|
+
# Check if worktree already exists
|
|
325
|
+
if File.exist?(worktree_path)
|
|
326
|
+
puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
327
|
+
@created_worktrees[worktree_key] = worktree_path
|
|
328
|
+
return
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Ensure parent directory exists with proper error handling
|
|
332
|
+
begin
|
|
333
|
+
FileUtils.mkdir_p(File.dirname(worktree_path))
|
|
334
|
+
rescue Errno::EACCES => e
|
|
335
|
+
raise Error, "Permission denied creating worktree directory: #{e.message}"
|
|
336
|
+
rescue Errno::ENOSPC => e
|
|
337
|
+
raise Error, "Not enough disk space for worktree: #{e.message}"
|
|
338
|
+
rescue StandardError => e
|
|
339
|
+
raise Error, "Failed to create worktree directory: #{e.message}"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Get current branch
|
|
343
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
344
|
+
raise Error, "Failed to get current branch in #{repo_root}: #{output}" unless status.success?
|
|
345
|
+
|
|
346
|
+
current_branch = output.strip
|
|
347
|
+
|
|
348
|
+
# Create worktree with a new branch based on current branch
|
|
349
|
+
branch_name = worktree_name
|
|
350
|
+
puts "Creating worktree: #{worktree_path} with branch: #{branch_name}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
351
|
+
|
|
352
|
+
# Create worktree with a new branch
|
|
353
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", "-b", branch_name, worktree_path, current_branch)
|
|
354
|
+
|
|
355
|
+
# If branch already exists, try without -b flag
|
|
356
|
+
if !status.success? && output.include?("already exists")
|
|
357
|
+
puts "Branch #{branch_name} already exists, using existing branch" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
358
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# If worktree path is already in use, it might be from a previous run
|
|
362
|
+
if !status.success? && output.include?("is already used by worktree")
|
|
363
|
+
puts "Worktree path already in use, checking if it's valid" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
364
|
+
# Check if the worktree actually exists at that path
|
|
365
|
+
if File.exist?(File.join(worktree_path, ".git"))
|
|
366
|
+
puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
367
|
+
@created_worktrees[worktree_key] = worktree_path
|
|
368
|
+
return
|
|
369
|
+
else
|
|
370
|
+
# The worktree is registered but the directory doesn't exist, prune and retry
|
|
371
|
+
puts "Pruning stale worktree references" unless ENV["CLAUDE_SWARM_PROMPT"]
|
|
372
|
+
begin
|
|
373
|
+
system!("git", "-C", repo_root, "worktree", "prune")
|
|
374
|
+
rescue Error
|
|
375
|
+
# Ignore errors when pruning
|
|
376
|
+
end
|
|
377
|
+
output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
raise Error, "Failed to create worktree: #{output}" unless status.success?
|
|
382
|
+
|
|
383
|
+
@created_worktrees[worktree_key] = worktree_path
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def has_uncommitted_changes?(worktree_path)
|
|
387
|
+
# Check if there are any uncommitted changes (staged or unstaged)
|
|
388
|
+
output, status = Open3.capture2e("git", "-C", worktree_path, "status", "--porcelain")
|
|
389
|
+
return false unless status.success?
|
|
390
|
+
|
|
391
|
+
# If output is not empty, there are changes
|
|
392
|
+
!output.strip.empty?
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def has_unpushed_commits?(worktree_path)
|
|
396
|
+
# Get the current branch
|
|
397
|
+
branch_output, branch_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "HEAD")
|
|
398
|
+
return false unless branch_status.success?
|
|
399
|
+
|
|
400
|
+
current_branch = branch_output.strip
|
|
401
|
+
|
|
402
|
+
# Check if the branch has an upstream
|
|
403
|
+
_, upstream_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "#{current_branch}@{upstream}")
|
|
404
|
+
|
|
405
|
+
# If branch has upstream, check against it
|
|
406
|
+
if upstream_status.success?
|
|
407
|
+
# Check for unpushed commits against upstream
|
|
408
|
+
unpushed_output, unpushed_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{current_branch}@{upstream}")
|
|
409
|
+
return false unless unpushed_status.success?
|
|
410
|
+
|
|
411
|
+
# If output is not empty, there are unpushed commits
|
|
412
|
+
return !unpushed_output.strip.empty?
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# No upstream - this is likely a new branch created by the worktree
|
|
416
|
+
# The key insight: when git worktree add -b creates a branch, it creates it in BOTH
|
|
417
|
+
# the worktree AND the main repository. So we need to check the reflog in the worktree
|
|
418
|
+
# to see if any NEW commits were made after the worktree was created.
|
|
419
|
+
|
|
420
|
+
# Use reflog to check if any commits were made in this worktree
|
|
421
|
+
reflog_output, reflog_status = Open3.capture2e("git", "-C", worktree_path, "reflog", "--format=%H %gs", current_branch)
|
|
422
|
+
|
|
423
|
+
if reflog_status.success? && !reflog_output.strip.empty?
|
|
424
|
+
reflog_lines = reflog_output.strip.split("\n")
|
|
425
|
+
|
|
426
|
+
# Look for commit entries (not branch creation)
|
|
427
|
+
commit_entries = reflog_lines.select { |line| line.include?(" commit:") || line.include?(" commit (amend):") }
|
|
428
|
+
|
|
429
|
+
# If there are commit entries in the reflog, there are unpushed commits
|
|
430
|
+
return !commit_entries.empty?
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# As a fallback, assume no unpushed commits
|
|
434
|
+
false
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def find_original_repo_for_worktree(worktree_path)
|
|
438
|
+
# Get the git directory for this worktree
|
|
439
|
+
_, git_dir_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--git-dir")
|
|
440
|
+
return unless git_dir_status.success?
|
|
441
|
+
|
|
442
|
+
# Read the gitdir file to find the main repository
|
|
443
|
+
# Worktree .git files contain: gitdir: /path/to/main/repo/.git/worktrees/worktree-name
|
|
444
|
+
if File.file?(File.join(worktree_path, ".git"))
|
|
445
|
+
gitdir_content = File.read(File.join(worktree_path, ".git")).strip
|
|
446
|
+
if gitdir_content =~ /^gitdir: (.+)$/
|
|
447
|
+
git_path = ::Regexp.last_match(1)
|
|
448
|
+
# Extract the main repo path from the worktree git path
|
|
449
|
+
# Format: /path/to/repo/.git/worktrees/worktree-name
|
|
450
|
+
return ::Regexp.last_match(1) if git_path =~ %r{^(.+)/\.git/worktrees/[^/]+$}
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def find_base_branch(repo_path)
|
|
458
|
+
# Try to find the base branch - check for main, master, or the default branch
|
|
459
|
+
["main", "master"].each do |branch|
|
|
460
|
+
_, status = Open3.capture2e("git", "-C", repo_path, "rev-parse", "--verify", "refs/heads/#{branch}")
|
|
461
|
+
return branch if status.success?
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Try to get the default branch from HEAD
|
|
465
|
+
output, status = Open3.capture2e("git", "-C", repo_path, "symbolic-ref", "refs/remotes/origin/HEAD")
|
|
466
|
+
if status.success?
|
|
467
|
+
# Extract branch name from refs/remotes/origin/main
|
|
468
|
+
branch_match = output.strip.match(%r{refs/remotes/origin/(.+)$})
|
|
469
|
+
return branch_match[1] if branch_match
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
nil
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
# Provides consistent YAML loading across the application
|
|
5
|
+
module YamlLoader
|
|
6
|
+
class << self
|
|
7
|
+
# Load a YAML configuration file (enables aliases for configuration flexibility)
|
|
8
|
+
# @param file_path [String] Path to the configuration file
|
|
9
|
+
# @return [Hash] The loaded configuration
|
|
10
|
+
# @raise [ClaudeSwarm::Error] Re-raises with a more descriptive error message
|
|
11
|
+
def load_config_file(file_path)
|
|
12
|
+
YAML.load_file(file_path, aliases: true)
|
|
13
|
+
rescue Errno::ENOENT
|
|
14
|
+
raise ClaudeSwarm::Error, "Configuration file not found: #{file_path}"
|
|
15
|
+
rescue Psych::SyntaxError => e
|
|
16
|
+
raise ClaudeSwarm::Error, "Invalid YAML syntax in #{file_path}: #{e.message}"
|
|
17
|
+
rescue Psych::BadAlias => e
|
|
18
|
+
raise ClaudeSwarm::Error, "Invalid YAML alias in #{file_path}: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/claude_swarm.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Standard library dependencies
|
|
4
|
+
require "bundler"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "English"
|
|
7
|
+
require "erb"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "io/console"
|
|
10
|
+
require "json"
|
|
11
|
+
require "logger"
|
|
12
|
+
require "open3"
|
|
13
|
+
require "pathname"
|
|
14
|
+
require "pty"
|
|
15
|
+
require "securerandom"
|
|
16
|
+
require "set"
|
|
17
|
+
require "shellwords"
|
|
18
|
+
require "time"
|
|
19
|
+
require "timeout"
|
|
20
|
+
require "tmpdir"
|
|
21
|
+
require "yaml"
|
|
22
|
+
|
|
23
|
+
# External dependencies
|
|
24
|
+
require "claude_sdk"
|
|
25
|
+
require "fast_mcp"
|
|
26
|
+
require "mcp_client"
|
|
27
|
+
require "thor"
|
|
28
|
+
|
|
29
|
+
# Zeitwerk setup
|
|
30
|
+
require "zeitwerk"
|
|
31
|
+
loader = Zeitwerk::Loader.new
|
|
32
|
+
loader.tag = "claude_swarm"
|
|
33
|
+
loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
|
|
34
|
+
loader.ignore("#{__dir__}/claude_swarm/templates")
|
|
35
|
+
loader.inflector.inflect(
|
|
36
|
+
"cli" => "CLI",
|
|
37
|
+
"openai" => "OpenAI",
|
|
38
|
+
)
|
|
39
|
+
loader.setup
|
|
40
|
+
|
|
41
|
+
module ClaudeSwarm
|
|
42
|
+
class Error < StandardError; end
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
def root_dir
|
|
46
|
+
ENV.fetch("CLAUDE_SWARM_ROOT_DIR") { Dir.pwd }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def home_dir
|
|
50
|
+
ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def joined_home_dir(*strings)
|
|
54
|
+
File.join(home_dir, *strings)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def joined_run_dir(*strings)
|
|
58
|
+
joined_home_dir("run", *strings)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def joined_sessions_dir(*strings)
|
|
62
|
+
joined_home_dir("sessions", *strings)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def joined_worktrees_dir(*strings)
|
|
66
|
+
joined_home_dir("worktrees", *strings)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|