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,363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
class Configuration
|
|
5
|
+
# Frozen constants for validation
|
|
6
|
+
VALID_PROVIDERS = ["claude", "openai"].freeze
|
|
7
|
+
OPENAI_SPECIFIC_FIELDS = ["temperature", "api_version", "openai_token_env", "base_url", "reasoning_effort"].freeze
|
|
8
|
+
VALID_API_VERSIONS = ["chat_completion", "responses"].freeze
|
|
9
|
+
VALID_REASONING_EFFORTS = ["low", "medium", "high"].freeze
|
|
10
|
+
|
|
11
|
+
# Regex patterns
|
|
12
|
+
ENV_VAR_PATTERN = /\$\{([^}]+)\}/
|
|
13
|
+
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
14
|
+
O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
|
|
15
|
+
|
|
16
|
+
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :root_directory
|
|
17
|
+
|
|
18
|
+
def initialize(config_path, base_dir: nil, options: {})
|
|
19
|
+
@config_path = Pathname.new(config_path).expand_path
|
|
20
|
+
@config_dir = @config_path.dirname
|
|
21
|
+
@base_dir = base_dir || @config_dir
|
|
22
|
+
@root_directory = @base_dir
|
|
23
|
+
@options = options
|
|
24
|
+
load_and_validate
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def main_instance_config
|
|
28
|
+
instances[main_instance]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def instance_names
|
|
32
|
+
instances.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def connections_for(instance_name)
|
|
36
|
+
instances[instance_name][:connections] || []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def before_commands
|
|
40
|
+
@swarm["before"] || []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def after_commands
|
|
44
|
+
@swarm["after"] || []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_directories
|
|
48
|
+
@instances.each do |name, instance|
|
|
49
|
+
# Validate all directories in the directories array
|
|
50
|
+
instance[:directories].each do |directory|
|
|
51
|
+
raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def has_before_commands?
|
|
59
|
+
@swarm && @swarm["before"] && !@swarm["before"].empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def load_and_validate
|
|
63
|
+
@config = YamlLoader.load_config_file(@config_path)
|
|
64
|
+
interpolate_env_vars!(@config)
|
|
65
|
+
validate_version
|
|
66
|
+
validate_swarm
|
|
67
|
+
parse_swarm
|
|
68
|
+
# Skip directory validation if before commands are present
|
|
69
|
+
# They might create the directories
|
|
70
|
+
validate_directories unless has_before_commands?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def interpolate_env_vars!(obj)
|
|
74
|
+
case obj
|
|
75
|
+
when String
|
|
76
|
+
interpolate_env_string(obj)
|
|
77
|
+
when Hash
|
|
78
|
+
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
|
79
|
+
when Array
|
|
80
|
+
obj.map! { |v| interpolate_env_vars!(v) }
|
|
81
|
+
else
|
|
82
|
+
obj
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def interpolate_env_string(str)
|
|
87
|
+
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
88
|
+
env_var = Regexp.last_match(1)
|
|
89
|
+
has_default = Regexp.last_match(2)
|
|
90
|
+
default_value = Regexp.last_match(3)
|
|
91
|
+
|
|
92
|
+
if ENV.key?(env_var)
|
|
93
|
+
ENV[env_var]
|
|
94
|
+
elsif has_default
|
|
95
|
+
default_value || ""
|
|
96
|
+
else
|
|
97
|
+
raise Error, "Environment variable '#{env_var}' is not set"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_version
|
|
103
|
+
version = @config["version"]
|
|
104
|
+
raise Error, "Missing 'version' field in configuration" unless version
|
|
105
|
+
raise Error, "Unsupported version: #{version}. Only version 1 is supported" unless version == 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_swarm
|
|
109
|
+
raise Error, "Missing 'swarm' field in configuration" unless @config["swarm"]
|
|
110
|
+
|
|
111
|
+
swarm = @config["swarm"]
|
|
112
|
+
raise Error, "Missing 'name' field in swarm configuration" unless swarm["name"]
|
|
113
|
+
raise Error, "Missing 'instances' field in swarm configuration" unless swarm["instances"]
|
|
114
|
+
raise Error, "Missing 'main' field in swarm configuration" unless swarm["main"]
|
|
115
|
+
|
|
116
|
+
raise Error, "No instances defined" if swarm["instances"].empty?
|
|
117
|
+
|
|
118
|
+
main = swarm["main"]
|
|
119
|
+
raise Error, "Main instance '#{main}' not found in instances" unless swarm["instances"].key?(main)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_swarm
|
|
123
|
+
@swarm = @config["swarm"]
|
|
124
|
+
@swarm_name = @swarm["name"]
|
|
125
|
+
@main_instance = @swarm["main"]
|
|
126
|
+
@instances = {}
|
|
127
|
+
@swarm["instances"].each do |name, config|
|
|
128
|
+
@instances[name] = parse_instance(name, config)
|
|
129
|
+
end
|
|
130
|
+
validate_main_instance_provider
|
|
131
|
+
validate_connections
|
|
132
|
+
detect_circular_dependencies
|
|
133
|
+
validate_openai_env_vars
|
|
134
|
+
validate_openai_responses_api_compatibility
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_instance(name, config)
|
|
138
|
+
config ||= {}
|
|
139
|
+
|
|
140
|
+
# Validate required fields
|
|
141
|
+
raise Error, "Instance '#{name}' missing required 'description' field" unless config["description"]
|
|
142
|
+
|
|
143
|
+
# Parse provider (optional, defaults to claude)
|
|
144
|
+
provider = config["provider"]
|
|
145
|
+
model = config["model"]
|
|
146
|
+
|
|
147
|
+
# Validate provider value if specified
|
|
148
|
+
if provider && !VALID_PROVIDERS.include?(provider)
|
|
149
|
+
raise Error, "Instance '#{name}' has invalid provider '#{provider}'. Must be 'claude' or 'openai'"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Validate reasoning_effort for OpenAI provider
|
|
153
|
+
if config["reasoning_effort"]
|
|
154
|
+
# Ensure it's only used with OpenAI provider
|
|
155
|
+
if provider != "openai"
|
|
156
|
+
raise Error, "Instance '#{name}' has reasoning_effort but provider is not 'openai'"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Validate the value
|
|
160
|
+
unless VALID_REASONING_EFFORTS.include?(config["reasoning_effort"])
|
|
161
|
+
raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Validate it's only used with o-series or gpt-5 models
|
|
165
|
+
# Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.
|
|
166
|
+
unless model&.match?(O_SERIES_MODEL_PATTERN)
|
|
167
|
+
raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series or gpt-5 model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.)"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Validate temperature is not used with o-series or gpt-5 models when provider is openai
|
|
172
|
+
if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
|
|
173
|
+
raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series or gpt-5 model. O-series and gpt-5 models use deterministic reasoning and don't accept temperature settings"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Validate OpenAI-specific fields only when provider is not "openai"
|
|
177
|
+
if provider != "openai"
|
|
178
|
+
invalid_fields = OPENAI_SPECIFIC_FIELDS & config.keys
|
|
179
|
+
unless invalid_fields.empty?
|
|
180
|
+
raise Error, "Instance '#{name}' has OpenAI-specific fields #{invalid_fields.join(", ")} but provider is not 'openai'"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Validate api_version if specified
|
|
185
|
+
if config["api_version"] && !VALID_API_VERSIONS.include?(config["api_version"])
|
|
186
|
+
raise Error, "Instance '#{name}' has invalid api_version '#{config["api_version"]}'. Must be 'chat_completion' or 'responses'"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Validate tool fields are arrays if present
|
|
190
|
+
validate_tool_field(name, config, "tools")
|
|
191
|
+
validate_tool_field(name, config, "allowed_tools")
|
|
192
|
+
validate_tool_field(name, config, "disallowed_tools")
|
|
193
|
+
|
|
194
|
+
# Support both 'tools' (deprecated) and 'allowed_tools' for backward compatibility
|
|
195
|
+
allowed_tools = config["allowed_tools"] || config["tools"] || []
|
|
196
|
+
|
|
197
|
+
# Parse directory field - support both string and array
|
|
198
|
+
directories = parse_directories(config["directory"])
|
|
199
|
+
|
|
200
|
+
instance_config = {
|
|
201
|
+
name: name,
|
|
202
|
+
directory: directories.first, # Keep single directory for backward compatibility
|
|
203
|
+
directories: directories, # New field with all directories
|
|
204
|
+
model: config["model"] || "sonnet",
|
|
205
|
+
connections: Array(config["connections"]),
|
|
206
|
+
tools: Array(allowed_tools), # Keep as 'tools' internally for compatibility
|
|
207
|
+
allowed_tools: Array(allowed_tools),
|
|
208
|
+
disallowed_tools: Array(config["disallowed_tools"]),
|
|
209
|
+
mcps: parse_mcps(config["mcps"] || []),
|
|
210
|
+
prompt: config["prompt"],
|
|
211
|
+
prompt_file: config["prompt_file"],
|
|
212
|
+
description: config["description"],
|
|
213
|
+
vibe: config["vibe"],
|
|
214
|
+
worktree: parse_worktree_value(config["worktree"]),
|
|
215
|
+
provider: provider, # nil means Claude (default)
|
|
216
|
+
hooks: config["hooks"], # Pass hooks configuration as-is
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Add OpenAI-specific fields only when provider is "openai"
|
|
220
|
+
if provider == "openai"
|
|
221
|
+
instance_config[:temperature] = config["temperature"] if config["temperature"]
|
|
222
|
+
instance_config[:api_version] = config["api_version"] || "chat_completion"
|
|
223
|
+
instance_config[:openai_token_env] = config["openai_token_env"] || "OPENAI_API_KEY"
|
|
224
|
+
instance_config[:base_url] = config["base_url"]
|
|
225
|
+
instance_config[:reasoning_effort] = config["reasoning_effort"] if config["reasoning_effort"]
|
|
226
|
+
# Default vibe to true for OpenAI instances if not specified
|
|
227
|
+
instance_config[:vibe] = true if config["vibe"].nil?
|
|
228
|
+
elsif config["vibe"].nil?
|
|
229
|
+
# Default vibe to false for Claude instances if not specified
|
|
230
|
+
instance_config[:vibe] = false
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
instance_config
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def parse_mcps(mcps)
|
|
237
|
+
mcps.map do |mcp|
|
|
238
|
+
validate_mcp(mcp)
|
|
239
|
+
mcp
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def validate_mcp(mcp)
|
|
244
|
+
raise Error, "MCP configuration missing 'name'" unless mcp["name"]
|
|
245
|
+
|
|
246
|
+
case mcp["type"]
|
|
247
|
+
when "stdio"
|
|
248
|
+
raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
|
|
249
|
+
when "sse", "http"
|
|
250
|
+
raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
|
|
251
|
+
else
|
|
252
|
+
raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def validate_connections
|
|
257
|
+
@instances.each do |name, instance|
|
|
258
|
+
instance[:connections].each do |connection|
|
|
259
|
+
raise Error, "Instance '#{name}' has connection to unknown instance '#{connection}'" unless @instances.key?(connection)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def detect_circular_dependencies
|
|
265
|
+
@instances.each_key do |instance_name|
|
|
266
|
+
visited = Set.new
|
|
267
|
+
path = []
|
|
268
|
+
detect_cycle_from(instance_name, visited, path)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def detect_cycle_from(instance_name, visited, path)
|
|
273
|
+
return if visited.include?(instance_name)
|
|
274
|
+
|
|
275
|
+
if path.include?(instance_name)
|
|
276
|
+
cycle_start = path.index(instance_name)
|
|
277
|
+
cycle = path[cycle_start..] + [instance_name]
|
|
278
|
+
raise Error, "Circular dependency detected: #{cycle.join(" -> ")}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
path.push(instance_name)
|
|
282
|
+
@instances[instance_name][:connections].each do |connection|
|
|
283
|
+
detect_cycle_from(connection, visited, path)
|
|
284
|
+
end
|
|
285
|
+
path.pop
|
|
286
|
+
visited.add(instance_name)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def validate_tool_field(instance_name, config, field_name)
|
|
290
|
+
return unless config.key?(field_name)
|
|
291
|
+
|
|
292
|
+
field_value = config[field_name]
|
|
293
|
+
raise Error, "Instance '#{instance_name}' field '#{field_name}' must be an array, got #{field_value.class.name}" unless field_value.is_a?(Array)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def parse_directories(directory_config)
|
|
297
|
+
# Default to current directory if not specified
|
|
298
|
+
directory_config ||= "."
|
|
299
|
+
|
|
300
|
+
# Convert to array and expand paths
|
|
301
|
+
directories = Array(directory_config).map { |dir| expand_path(dir) }
|
|
302
|
+
|
|
303
|
+
# Ensure at least one directory
|
|
304
|
+
directories.empty? ? [expand_path(".")] : directories
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def expand_path(path)
|
|
308
|
+
Pathname.new(path).expand_path(@base_dir).to_s
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def parse_worktree_value(value)
|
|
312
|
+
return if value.nil? # Omitted means follow CLI behavior
|
|
313
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
314
|
+
return value.to_s if value.is_a?(String) && !value.empty?
|
|
315
|
+
|
|
316
|
+
raise Error, "Invalid worktree value: #{value.inspect}. Must be true, false, or a non-empty string"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def validate_openai_env_vars
|
|
320
|
+
@instances.each_value do |instance|
|
|
321
|
+
next unless instance[:provider] == "openai"
|
|
322
|
+
|
|
323
|
+
env_var = instance[:openai_token_env]
|
|
324
|
+
unless ENV.key?(env_var) && !ENV[env_var].to_s.strip.empty?
|
|
325
|
+
raise Error, "Environment variable '#{env_var}' is not set. OpenAI provider instances require an API key."
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def validate_main_instance_provider
|
|
331
|
+
# Only validate in interactive mode (when no prompt is provided)
|
|
332
|
+
return if @options[:prompt]
|
|
333
|
+
|
|
334
|
+
main_config = @instances[@main_instance]
|
|
335
|
+
if main_config[:provider]
|
|
336
|
+
raise Error, "Main instance '#{@main_instance}' cannot have a provider setting in interactive mode"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def validate_openai_responses_api_compatibility
|
|
341
|
+
# Check if any instance uses OpenAI provider with responses API
|
|
342
|
+
responses_api_instances = @instances.select do |_name, instance|
|
|
343
|
+
instance[:provider] == "openai" && instance[:api_version] == "responses"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
return if responses_api_instances.empty?
|
|
347
|
+
|
|
348
|
+
# Check ruby-openai version
|
|
349
|
+
begin
|
|
350
|
+
require "openai/version"
|
|
351
|
+
openai_version = Gem::Version.new(::OpenAI::VERSION)
|
|
352
|
+
required_version = Gem::Version.new("8.0.0")
|
|
353
|
+
|
|
354
|
+
if openai_version < required_version
|
|
355
|
+
instance_names = responses_api_instances.keys.join(", ")
|
|
356
|
+
raise Error, "Instances #{instance_names} use OpenAI provider with api_version 'responses', which requires ruby-openai >= 8.0. Current version is #{openai_version}. Please update your Gemfile or run: gem install ruby-openai -v '>= 8.0'"
|
|
357
|
+
end
|
|
358
|
+
rescue LoadError
|
|
359
|
+
# ruby-openai is not installed, which is fine - it will be caught later when trying to use it
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This hook is called when Claude Code starts a session
|
|
5
|
+
# It saves the transcript path for the main instance so the orchestrator can tail it
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
# Read input from stdin
|
|
11
|
+
begin
|
|
12
|
+
stdin_data = $stdin.read
|
|
13
|
+
input = JSON.parse(stdin_data)
|
|
14
|
+
rescue => e
|
|
15
|
+
# Return error response
|
|
16
|
+
puts JSON.generate({
|
|
17
|
+
"success" => false,
|
|
18
|
+
"error" => "Failed to read/parse input: #{e.message}",
|
|
19
|
+
})
|
|
20
|
+
exit(1)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get session path from command-line argument or environment
|
|
24
|
+
session_path = ARGV[0] || ENV["CLAUDE_SWARM_SESSION_PATH"]
|
|
25
|
+
|
|
26
|
+
if session_path && input["transcript_path"]
|
|
27
|
+
# Write the transcript path to a known location
|
|
28
|
+
path_file = File.join(session_path, "main_instance_transcript.path")
|
|
29
|
+
File.write(path_file, input["transcript_path"])
|
|
30
|
+
|
|
31
|
+
# Return success
|
|
32
|
+
puts JSON.generate({
|
|
33
|
+
"success" => true,
|
|
34
|
+
})
|
|
35
|
+
else
|
|
36
|
+
# Return error if missing required data
|
|
37
|
+
puts JSON.generate({
|
|
38
|
+
"success" => false,
|
|
39
|
+
"error" => "Missing session path or transcript path",
|
|
40
|
+
})
|
|
41
|
+
exit(1)
|
|
42
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
# Centralized JSON handling for the Claude Swarm codebase
|
|
5
|
+
class JsonHandler
|
|
6
|
+
class << self
|
|
7
|
+
# Parse JSON string into Ruby object
|
|
8
|
+
# @param json_string [String] The JSON string to parse
|
|
9
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
|
10
|
+
# @return [Object] The parsed Ruby object, or original string if parsing fails and raise_on_error is false
|
|
11
|
+
# @raise [JSON::ParserError] If the JSON is invalid and raise_on_error is true
|
|
12
|
+
def parse(json_string, raise_on_error: false)
|
|
13
|
+
JSON.parse(json_string)
|
|
14
|
+
rescue JSON::ParserError => e
|
|
15
|
+
raise e if raise_on_error
|
|
16
|
+
|
|
17
|
+
json_string
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse JSON string with exception raising
|
|
21
|
+
# @param json_string [String] The JSON string to parse
|
|
22
|
+
# @return [Object] The parsed Ruby object
|
|
23
|
+
# @raise [JSON::ParserError] If the JSON is invalid
|
|
24
|
+
def parse!(json_string)
|
|
25
|
+
parse(json_string, raise_on_error: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Parse JSON from a file with exception raising
|
|
29
|
+
# @param file_path [String] Path to the JSON file
|
|
30
|
+
# @return [Object] The parsed Ruby object
|
|
31
|
+
# @raise [Errno::ENOENT] If the file does not exist
|
|
32
|
+
# @raise [JSON::ParserError] If the file contains invalid JSON
|
|
33
|
+
def parse_file!(file_path)
|
|
34
|
+
content = File.read(file_path)
|
|
35
|
+
parse!(content)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Parse JSON from a file, returning nil on error
|
|
39
|
+
# @param file_path [String] Path to the JSON file
|
|
40
|
+
# @return [Object, nil] The parsed Ruby object or nil if file doesn't exist or contains invalid JSON
|
|
41
|
+
def parse_file(file_path)
|
|
42
|
+
parse_file!(file_path)
|
|
43
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Generate pretty-formatted JSON string
|
|
48
|
+
# @param object [Object] The Ruby object to convert to JSON
|
|
49
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
|
50
|
+
# @return [String, nil] The pretty-formatted JSON string, or nil if generation fails and raise_on_error is false
|
|
51
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON and raise_on_error is true
|
|
52
|
+
def pretty_generate(object, raise_on_error: false)
|
|
53
|
+
JSON.pretty_generate(object)
|
|
54
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
|
55
|
+
raise e if raise_on_error
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generate pretty-formatted JSON string with exception raising
|
|
61
|
+
# @param object [Object] The Ruby object to convert to JSON
|
|
62
|
+
# @return [String] The pretty-formatted JSON string
|
|
63
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
|
64
|
+
def pretty_generate!(object)
|
|
65
|
+
pretty_generate(object, raise_on_error: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Write Ruby object to a JSON file with pretty formatting
|
|
69
|
+
# @param file_path [String] Path to the JSON file
|
|
70
|
+
# @param object [Object] The Ruby object to write
|
|
71
|
+
# @return [Boolean] True if successful, false if generation or write fails
|
|
72
|
+
def write_file(file_path, object)
|
|
73
|
+
json_string = pretty_generate!(object)
|
|
74
|
+
File.write(file_path, json_string)
|
|
75
|
+
true
|
|
76
|
+
rescue JSON::GeneratorError, JSON::NestingError, SystemCallError
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Write Ruby object to a JSON file with exception raising
|
|
81
|
+
# @param file_path [String] Path to the JSON file
|
|
82
|
+
# @param object [Object] The Ruby object to write
|
|
83
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
|
84
|
+
# @raise [SystemCallError] If the file cannot be written
|
|
85
|
+
def write_file!(file_path, object)
|
|
86
|
+
json_string = pretty_generate!(object)
|
|
87
|
+
File.write(file_path, json_string)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|