claude_swarm 0.1.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -66
- data/.rubocop_todo.yml +11 -0
- data/CHANGELOG.md +93 -0
- data/CLAUDE.md +61 -0
- data/README.md +172 -15
- data/Rakefile +1 -1
- data/examples/mixed-provider-swarm.yml +23 -0
- data/lib/claude_swarm/claude_code_executor.rb +7 -12
- data/lib/claude_swarm/claude_mcp_server.rb +26 -12
- data/lib/claude_swarm/cli.rb +293 -165
- data/lib/claude_swarm/commands/ps.rb +22 -24
- data/lib/claude_swarm/commands/show.rb +45 -63
- data/lib/claude_swarm/configuration.rb +137 -8
- data/lib/claude_swarm/mcp_generator.rb +39 -14
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +301 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +205 -39
- data/lib/claude_swarm/process_tracker.rb +7 -7
- data/lib/claude_swarm/session_cost_calculator.rb +93 -0
- data/lib/claude_swarm/session_path.rb +3 -5
- data/lib/claude_swarm/system_utils.rb +1 -3
- 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 +43 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +13 -20
- data/lib/claude_swarm.rb +23 -10
- data/single.yml +482 -6
- metadata +50 -16
- data/claude-swarm.yml +0 -64
- data/lib/claude_swarm/reset_session_tool.rb +0 -22
- data/lib/claude_swarm/session_info_tool.rb +0 -22
- data/lib/claude_swarm/task_tool.rb +0 -39
- /data/{example → examples}/claude-swarm.yml +0 -0
- /data/{example → examples}/microservices-team.yml +0 -0
- /data/{example → examples}/session-restoration-demo.yml +0 -0
- /data/{example → examples}/test-generation.yml +0 -0
@@ -1,9 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
require "json"
|
5
|
-
require "time"
|
6
|
-
|
7
3
|
module ClaudeSwarm
|
8
4
|
module Commands
|
9
5
|
class Ps
|
@@ -80,7 +76,7 @@ module ClaudeSwarm
|
|
80
76
|
|
81
77
|
# Load config for swarm name and main directory
|
82
78
|
config_file = File.join(session_dir, "config.yml")
|
83
|
-
return
|
79
|
+
return unless File.exist?(config_file)
|
84
80
|
|
85
81
|
config = YAML.load_file(config_file)
|
86
82
|
swarm_name = config.dig("swarm", "name") || "Unknown"
|
@@ -94,10 +90,10 @@ module ClaudeSwarm
|
|
94
90
|
# Get all directories - handle both string and array formats
|
95
91
|
dir_config = config.dig("swarm", "instances", main_instance, "directory")
|
96
92
|
directories = if dir_config.is_a?(Array)
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
93
|
+
dir_config
|
94
|
+
else
|
95
|
+
[dir_config || "."]
|
96
|
+
end
|
101
97
|
|
102
98
|
# Expand paths relative to the base directory
|
103
99
|
expanded_directories = directories.map do |dir|
|
@@ -110,10 +106,11 @@ module ClaudeSwarm
|
|
110
106
|
directories_str = expanded_directories.join(", ")
|
111
107
|
|
112
108
|
# Calculate total cost from JSON log
|
113
|
-
|
109
|
+
log_file = File.join(session_dir, "session.log.json")
|
110
|
+
total_cost = SessionCostCalculator.calculate_simple_total(log_file)
|
114
111
|
|
115
|
-
# Get uptime from directory creation time
|
116
|
-
start_time =
|
112
|
+
# Get uptime from session metadata or fallback to directory creation time
|
113
|
+
start_time = get_start_time(session_dir)
|
117
114
|
uptime = format_duration(Time.now - start_time)
|
118
115
|
|
119
116
|
{
|
@@ -122,24 +119,25 @@ module ClaudeSwarm
|
|
122
119
|
cost: total_cost,
|
123
120
|
uptime: uptime,
|
124
121
|
directory: directories_str,
|
125
|
-
start_time: start_time
|
122
|
+
start_time: start_time,
|
126
123
|
}
|
127
124
|
rescue StandardError
|
128
125
|
nil
|
129
126
|
end
|
130
127
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
data = JSON.parse(line)
|
138
|
-
total += data["event"]["total_cost_usd"] if data.dig("event", "type") == "result" && data.dig("event", "total_cost_usd")
|
139
|
-
rescue JSON::ParserError
|
140
|
-
next
|
128
|
+
def get_start_time(session_dir)
|
129
|
+
# Try to get from session metadata first
|
130
|
+
metadata_file = File.join(session_dir, "session_metadata.json")
|
131
|
+
if File.exist?(metadata_file)
|
132
|
+
metadata = JSON.parse(File.read(metadata_file))
|
133
|
+
return Time.parse(metadata["start_time"]) if metadata["start_time"]
|
141
134
|
end
|
142
|
-
|
135
|
+
|
136
|
+
# Fallback to directory creation time
|
137
|
+
File.stat(session_dir).ctime
|
138
|
+
rescue StandardError
|
139
|
+
# If anything fails, use directory creation time
|
140
|
+
File.stat(session_dir).ctime
|
143
141
|
end
|
144
142
|
|
145
143
|
def format_duration(seconds)
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
require "json"
|
5
|
-
|
6
3
|
module ClaudeSwarm
|
7
4
|
module Commands
|
8
5
|
class Show
|
@@ -10,7 +7,7 @@ module ClaudeSwarm
|
|
10
7
|
session_path = find_session_path(session_id)
|
11
8
|
unless session_path
|
12
9
|
puts "Session not found: #{session_id}"
|
13
|
-
exit
|
10
|
+
exit(1)
|
14
11
|
end
|
15
12
|
|
16
13
|
# Load config to get main instance name
|
@@ -18,19 +15,26 @@ module ClaudeSwarm
|
|
18
15
|
main_instance_name = config.dig("swarm", "main")
|
19
16
|
|
20
17
|
# Parse all events to build instance data
|
21
|
-
|
18
|
+
log_file = File.join(session_path, "session.log.json")
|
19
|
+
instances = SessionCostCalculator.parse_instance_hierarchy(log_file)
|
22
20
|
|
23
21
|
# Calculate total cost (excluding main if not available)
|
24
22
|
total_cost = instances.values.sum { |i| i[:cost] }
|
25
23
|
cost_display = if instances[main_instance_name] && instances[main_instance_name][:has_cost_data]
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
format("$%.4f", total_cost)
|
25
|
+
else
|
26
|
+
"#{format("$%.4f", total_cost)} (excluding main instance)"
|
27
|
+
end
|
30
28
|
|
31
29
|
# Display session info
|
32
30
|
puts "Session: #{session_id}"
|
31
|
+
puts "Session Path: #{session_path}"
|
33
32
|
puts "Swarm: #{config.dig("swarm", "name")}"
|
33
|
+
|
34
|
+
# Display runtime if available
|
35
|
+
runtime_info = get_runtime_info(session_path)
|
36
|
+
puts "Runtime: #{runtime_info}" if runtime_info
|
37
|
+
|
34
38
|
puts "Total Cost: #{cost_display}"
|
35
39
|
|
36
40
|
# Try to read start directory
|
@@ -71,58 +75,36 @@ module ClaudeSwarm
|
|
71
75
|
end
|
72
76
|
end
|
73
77
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
name: instance_name,
|
89
|
-
id: instance_id,
|
90
|
-
cost: 0.0,
|
91
|
-
calls: 0,
|
92
|
-
called_by: Set.new,
|
93
|
-
calls_to: Set.new,
|
94
|
-
has_cost_data: false
|
95
|
-
}
|
96
|
-
|
97
|
-
# Track relationships
|
98
|
-
if calling_instance && calling_instance != instance_name
|
99
|
-
instances[instance_name][:called_by] << calling_instance
|
100
|
-
|
101
|
-
instances[calling_instance] ||= {
|
102
|
-
name: calling_instance,
|
103
|
-
id: data["calling_instance_id"],
|
104
|
-
cost: 0.0,
|
105
|
-
calls: 0,
|
106
|
-
called_by: Set.new,
|
107
|
-
calls_to: Set.new,
|
108
|
-
has_cost_data: false
|
109
|
-
}
|
110
|
-
instances[calling_instance][:calls_to] << instance_name
|
111
|
-
end
|
112
|
-
|
113
|
-
# Track costs and calls
|
114
|
-
if data.dig("event", "type") == "result"
|
115
|
-
instances[instance_name][:calls] += 1
|
116
|
-
if (cost = data.dig("event", "total_cost_usd"))
|
117
|
-
instances[instance_name][:cost] += cost
|
118
|
-
instances[instance_name][:has_cost_data] = true
|
119
|
-
end
|
120
|
-
end
|
121
|
-
rescue JSON::ParserError
|
122
|
-
next
|
78
|
+
def get_runtime_info(session_path)
|
79
|
+
metadata_file = File.join(session_path, "session_metadata.json")
|
80
|
+
return unless File.exist?(metadata_file)
|
81
|
+
|
82
|
+
metadata = JSON.parse(File.read(metadata_file))
|
83
|
+
|
84
|
+
if metadata["duration_seconds"]
|
85
|
+
# Session has completed
|
86
|
+
format_duration(metadata["duration_seconds"])
|
87
|
+
elsif metadata["start_time"]
|
88
|
+
# Session is still running or was interrupted
|
89
|
+
start_time = Time.parse(metadata["start_time"])
|
90
|
+
duration = (Time.now - start_time).to_i
|
91
|
+
"#{format_duration(duration)} (active)"
|
123
92
|
end
|
93
|
+
rescue StandardError
|
94
|
+
nil
|
95
|
+
end
|
124
96
|
|
125
|
-
|
97
|
+
def format_duration(seconds)
|
98
|
+
hours = seconds / 3600
|
99
|
+
minutes = (seconds % 3600) / 60
|
100
|
+
secs = seconds % 60
|
101
|
+
|
102
|
+
parts = []
|
103
|
+
parts << "#{hours}h" if hours.positive?
|
104
|
+
parts << "#{minutes}m" if minutes.positive?
|
105
|
+
parts << "#{secs}s"
|
106
|
+
|
107
|
+
parts.join(" ")
|
126
108
|
end
|
127
109
|
|
128
110
|
def display_instance_tree(instance, all_instances, level, main_instance_name)
|
@@ -137,10 +119,10 @@ module ClaudeSwarm
|
|
137
119
|
|
138
120
|
# Display cost - show n/a for main instance without cost data
|
139
121
|
cost_display = if instance[:name] == main_instance_name && !instance[:has_cost_data]
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
122
|
+
"n/a (interactive)"
|
123
|
+
else
|
124
|
+
format("$%.4f", instance[:cost])
|
125
|
+
end
|
144
126
|
|
145
127
|
puts "#{indent} Cost: #{cost_display} | Calls: #{instance[:calls]}"
|
146
128
|
|
@@ -1,16 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
require "pathname"
|
5
|
-
|
6
3
|
module ClaudeSwarm
|
7
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)?$/
|
15
|
+
|
8
16
|
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
|
9
17
|
|
10
|
-
def initialize(config_path, base_dir: nil)
|
18
|
+
def initialize(config_path, base_dir: nil, options: {})
|
11
19
|
@config_path = Pathname.new(config_path).expand_path
|
12
20
|
@config_dir = @config_path.dirname
|
13
21
|
@base_dir = base_dir || @config_dir
|
22
|
+
@options = options
|
14
23
|
load_and_validate
|
15
24
|
end
|
16
25
|
|
@@ -30,10 +39,15 @@ module ClaudeSwarm
|
|
30
39
|
@swarm["before"] || []
|
31
40
|
end
|
32
41
|
|
42
|
+
def after_commands
|
43
|
+
@swarm["after"] || []
|
44
|
+
end
|
45
|
+
|
33
46
|
private
|
34
47
|
|
35
48
|
def load_and_validate
|
36
49
|
@config = YAML.load_file(@config_path)
|
50
|
+
interpolate_env_vars!(@config)
|
37
51
|
validate_version
|
38
52
|
validate_swarm
|
39
53
|
parse_swarm
|
@@ -44,6 +58,35 @@ module ClaudeSwarm
|
|
44
58
|
raise Error, "Invalid YAML syntax: #{e.message}"
|
45
59
|
end
|
46
60
|
|
61
|
+
def interpolate_env_vars!(obj)
|
62
|
+
case obj
|
63
|
+
when String
|
64
|
+
interpolate_env_string(obj)
|
65
|
+
when Hash
|
66
|
+
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
67
|
+
when Array
|
68
|
+
obj.map! { |v| interpolate_env_vars!(v) }
|
69
|
+
else
|
70
|
+
obj
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def interpolate_env_string(str)
|
75
|
+
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
76
|
+
env_var = Regexp.last_match(1)
|
77
|
+
has_default = Regexp.last_match(2)
|
78
|
+
default_value = Regexp.last_match(3)
|
79
|
+
|
80
|
+
if ENV.key?(env_var)
|
81
|
+
ENV[env_var]
|
82
|
+
elsif has_default
|
83
|
+
default_value || ""
|
84
|
+
else
|
85
|
+
raise Error, "Environment variable '#{env_var}' is not set"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
47
90
|
def validate_version
|
48
91
|
version = @config["version"]
|
49
92
|
raise Error, "Missing 'version' field in configuration" unless version
|
@@ -72,8 +115,10 @@ module ClaudeSwarm
|
|
72
115
|
@swarm["instances"].each do |name, config|
|
73
116
|
@instances[name] = parse_instance(name, config)
|
74
117
|
end
|
118
|
+
validate_main_instance_provider
|
75
119
|
validate_connections
|
76
120
|
detect_circular_dependencies
|
121
|
+
validate_openai_env_vars
|
77
122
|
end
|
78
123
|
|
79
124
|
def parse_instance(name, config)
|
@@ -82,6 +127,52 @@ module ClaudeSwarm
|
|
82
127
|
# Validate required fields
|
83
128
|
raise Error, "Instance '#{name}' missing required 'description' field" unless config["description"]
|
84
129
|
|
130
|
+
# Parse provider (optional, defaults to claude)
|
131
|
+
provider = config["provider"]
|
132
|
+
model = config["model"]
|
133
|
+
|
134
|
+
# Validate provider value if specified
|
135
|
+
if provider && !VALID_PROVIDERS.include?(provider)
|
136
|
+
raise Error, "Instance '#{name}' has invalid provider '#{provider}'. Must be 'claude' or 'openai'"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validate reasoning_effort for OpenAI provider
|
140
|
+
if config["reasoning_effort"]
|
141
|
+
# Ensure it's only used with OpenAI provider
|
142
|
+
if provider != "openai"
|
143
|
+
raise Error, "Instance '#{name}' has reasoning_effort but provider is not 'openai'"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Validate the value
|
147
|
+
unless VALID_REASONING_EFFORTS.include?(config["reasoning_effort"])
|
148
|
+
raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
|
149
|
+
end
|
150
|
+
|
151
|
+
# Validate it's only used with o-series models
|
152
|
+
# Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, etc.
|
153
|
+
unless model&.match?(O_SERIES_MODEL_PATTERN)
|
154
|
+
raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Validate temperature is not used with o-series models when provider is openai
|
159
|
+
if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
|
160
|
+
raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series model. O-series models use deterministic reasoning and don't accept temperature settings"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate OpenAI-specific fields only when provider is not "openai"
|
164
|
+
if provider != "openai"
|
165
|
+
invalid_fields = OPENAI_SPECIFIC_FIELDS & config.keys
|
166
|
+
unless invalid_fields.empty?
|
167
|
+
raise Error, "Instance '#{name}' has OpenAI-specific fields #{invalid_fields.join(", ")} but provider is not 'openai'"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Validate api_version if specified
|
172
|
+
if config["api_version"] && !VALID_API_VERSIONS.include?(config["api_version"])
|
173
|
+
raise Error, "Instance '#{name}' has invalid api_version '#{config["api_version"]}'. Must be 'chat_completion' or 'responses'"
|
174
|
+
end
|
175
|
+
|
85
176
|
# Validate tool fields are arrays if present
|
86
177
|
validate_tool_field(name, config, "tools")
|
87
178
|
validate_tool_field(name, config, "allowed_tools")
|
@@ -93,7 +184,7 @@ module ClaudeSwarm
|
|
93
184
|
# Parse directory field - support both string and array
|
94
185
|
directories = parse_directories(config["directory"])
|
95
186
|
|
96
|
-
{
|
187
|
+
instance_config = {
|
97
188
|
name: name,
|
98
189
|
directory: directories.first, # Keep single directory for backward compatibility
|
99
190
|
directories: directories, # New field with all directories
|
@@ -105,9 +196,26 @@ module ClaudeSwarm
|
|
105
196
|
mcps: parse_mcps(config["mcps"] || []),
|
106
197
|
prompt: config["prompt"],
|
107
198
|
description: config["description"],
|
108
|
-
vibe: config["vibe"]
|
109
|
-
worktree: parse_worktree_value(config["worktree"])
|
199
|
+
vibe: config["vibe"],
|
200
|
+
worktree: parse_worktree_value(config["worktree"]),
|
201
|
+
provider: provider, # nil means Claude (default)
|
110
202
|
}
|
203
|
+
|
204
|
+
# Add OpenAI-specific fields only when provider is "openai"
|
205
|
+
if provider == "openai"
|
206
|
+
instance_config[:temperature] = config["temperature"] if config["temperature"]
|
207
|
+
instance_config[:api_version] = config["api_version"] || "chat_completion"
|
208
|
+
instance_config[:openai_token_env] = config["openai_token_env"] || "OPENAI_API_KEY"
|
209
|
+
instance_config[:base_url] = config["base_url"]
|
210
|
+
instance_config[:reasoning_effort] = config["reasoning_effort"] if config["reasoning_effort"]
|
211
|
+
# Default vibe to true for OpenAI instances if not specified
|
212
|
+
instance_config[:vibe] = true if config["vibe"].nil?
|
213
|
+
elsif config["vibe"].nil?
|
214
|
+
# Default vibe to false for Claude instances if not specified
|
215
|
+
instance_config[:vibe] = false
|
216
|
+
end
|
217
|
+
|
218
|
+
instance_config
|
111
219
|
end
|
112
220
|
|
113
221
|
def parse_mcps(mcps)
|
@@ -195,11 +303,32 @@ module ClaudeSwarm
|
|
195
303
|
end
|
196
304
|
|
197
305
|
def parse_worktree_value(value)
|
198
|
-
return
|
306
|
+
return if value.nil? # Omitted means follow CLI behavior
|
199
307
|
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
200
308
|
return value.to_s if value.is_a?(String) && !value.empty?
|
201
309
|
|
202
310
|
raise Error, "Invalid worktree value: #{value.inspect}. Must be true, false, or a non-empty string"
|
203
311
|
end
|
312
|
+
|
313
|
+
def validate_openai_env_vars
|
314
|
+
@instances.each_value do |instance|
|
315
|
+
next unless instance[:provider] == "openai"
|
316
|
+
|
317
|
+
env_var = instance[:openai_token_env]
|
318
|
+
unless ENV.key?(env_var) && !ENV[env_var].to_s.strip.empty?
|
319
|
+
raise Error, "Environment variable '#{env_var}' is not set. OpenAI provider instances require an API key."
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def validate_main_instance_provider
|
325
|
+
# Only validate in interactive mode (when no prompt is provided)
|
326
|
+
return if @options[:prompt]
|
327
|
+
|
328
|
+
main_config = @instances[@main_instance]
|
329
|
+
if main_config[:provider]
|
330
|
+
raise Error, "Main instance '#{@main_instance}' cannot have a provider setting in interactive mode"
|
331
|
+
end
|
332
|
+
end
|
204
333
|
end
|
205
334
|
end
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json"
|
4
|
-
require "fileutils"
|
5
|
-
require "shellwords"
|
6
|
-
require "securerandom"
|
7
|
-
|
8
3
|
module ClaudeSwarm
|
9
4
|
class McpGenerator
|
10
5
|
def initialize(configuration, vibe: false, restore_session_path: nil)
|
@@ -62,15 +57,20 @@ module ClaudeSwarm
|
|
62
57
|
instance[:connections].each do |connection_name|
|
63
58
|
connected_instance = @config.instances[connection_name]
|
64
59
|
mcp_servers[connection_name] = build_instance_mcp_config(
|
65
|
-
connection_name,
|
66
|
-
|
60
|
+
connection_name,
|
61
|
+
connected_instance,
|
62
|
+
calling_instance: name,
|
63
|
+
calling_instance_id: @instance_ids[name],
|
67
64
|
)
|
68
65
|
end
|
69
66
|
|
67
|
+
# Add Claude tools MCP server for OpenAI instances
|
68
|
+
mcp_servers["claude_tools"] = build_claude_tools_mcp_config if instance[:provider] == "openai"
|
69
|
+
|
70
70
|
config = {
|
71
71
|
"instance_id" => @instance_ids[name],
|
72
72
|
"instance_name" => name,
|
73
|
-
"mcpServers" => mcp_servers
|
73
|
+
"mcpServers" => mcp_servers,
|
74
74
|
}
|
75
75
|
|
76
76
|
File.write(mcp_config_path(name), JSON.pretty_generate(config))
|
@@ -82,18 +82,26 @@ module ClaudeSwarm
|
|
82
82
|
{
|
83
83
|
"type" => "stdio",
|
84
84
|
"command" => mcp["command"],
|
85
|
-
"args" => mcp["args"] || []
|
85
|
+
"args" => mcp["args"] || [],
|
86
86
|
}.tap do |config|
|
87
87
|
config["env"] = mcp["env"] if mcp["env"]
|
88
88
|
end
|
89
89
|
when "sse"
|
90
90
|
{
|
91
91
|
"type" => "sse",
|
92
|
-
"url" => mcp["url"]
|
92
|
+
"url" => mcp["url"],
|
93
93
|
}
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
+
def build_claude_tools_mcp_config
|
98
|
+
{
|
99
|
+
"type" => "stdio",
|
100
|
+
"command" => "claude",
|
101
|
+
"args" => ["mcp", "serve"],
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
97
105
|
def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
|
98
106
|
# Get the path to the claude-swarm executable
|
99
107
|
exe_path = "claude-swarm"
|
@@ -101,9 +109,12 @@ module ClaudeSwarm
|
|
101
109
|
# Build command-line arguments for Thor
|
102
110
|
args = [
|
103
111
|
"mcp-serve",
|
104
|
-
"--name",
|
105
|
-
|
106
|
-
"--
|
112
|
+
"--name",
|
113
|
+
name,
|
114
|
+
"--directory",
|
115
|
+
instance[:directory],
|
116
|
+
"--model",
|
117
|
+
instance[:model],
|
107
118
|
]
|
108
119
|
|
109
120
|
# Add directories array if we have multiple directories
|
@@ -130,6 +141,20 @@ module ClaudeSwarm
|
|
130
141
|
|
131
142
|
args.push("--vibe") if @vibe || instance[:vibe]
|
132
143
|
|
144
|
+
# Add provider-specific parameters
|
145
|
+
if instance[:provider]
|
146
|
+
args.push("--provider", instance[:provider])
|
147
|
+
|
148
|
+
# Add OpenAI-specific parameters
|
149
|
+
if instance[:provider] == "openai"
|
150
|
+
args.push("--reasoning-effort", instance[:reasoning_effort]) if instance[:reasoning_effort]
|
151
|
+
args.push("--temperature", instance[:temperature].to_s) if instance[:temperature]
|
152
|
+
args.push("--api-version", instance[:api_version]) if instance[:api_version]
|
153
|
+
args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
|
154
|
+
args.push("--base-url", instance[:base_url]) if instance[:base_url]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
133
158
|
# Add claude session ID if restoring
|
134
159
|
if @restore_states[name.to_s]
|
135
160
|
claude_session_id = @restore_states[name.to_s]["claude_session_id"]
|
@@ -139,7 +164,7 @@ module ClaudeSwarm
|
|
139
164
|
{
|
140
165
|
"type" => "stdio",
|
141
166
|
"command" => exe_path,
|
142
|
-
"args" => args
|
167
|
+
"args" => args,
|
143
168
|
}
|
144
169
|
end
|
145
170
|
|