claude_swarm 0.1.15 → 0.1.17

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.
@@ -0,0 +1,26 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Multi-Directory Example"
4
+ main: fullstack_dev
5
+ instances:
6
+ fullstack_dev:
7
+ description: "Full-stack developer with access to multiple project directories"
8
+ directory: [./frontend, ./backend, ./shared, ./docs]
9
+ model: opus
10
+ connections: [frontend_specialist, backend_specialist]
11
+ allowed_tools: [Read, Edit, Write, Bash, WebSearch]
12
+ prompt: "You are a full-stack developer with access to frontend, backend, shared code, and documentation directories"
13
+
14
+ frontend_specialist:
15
+ description: "Frontend developer focused on React components"
16
+ directory: ./frontend
17
+ model: sonnet
18
+ allowed_tools: [Read, Edit, Write, Bash]
19
+ prompt: "You specialize in React and frontend development"
20
+
21
+ backend_specialist:
22
+ description: "Backend developer focused on API development"
23
+ directory: ./backend
24
+ model: sonnet
25
+ allowed_tools: [Read, Edit, Write, Bash]
26
+ prompt: "You specialize in backend API development"
@@ -12,8 +12,9 @@ module ClaudeSwarm
12
12
 
13
13
  def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
14
14
  instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
15
- claude_session_id: nil)
15
+ claude_session_id: nil, additional_directories: [])
16
16
  @working_directory = working_directory
17
+ @additional_directories = additional_directories
17
18
  @model = model
18
19
  @mcp_config = mcp_config
19
20
  @vibe = vibe
@@ -259,6 +260,12 @@ module ClaudeSwarm
259
260
 
260
261
  cmd_array << "--verbose"
261
262
 
263
+ # Add additional directories with --add-dir
264
+ cmd_array << "--add-dir" if @additional_directories.any?
265
+ @additional_directories.each do |additional_dir|
266
+ cmd_array << additional_dir
267
+ end
268
+
262
269
  # Add MCP config if specified
263
270
  cmd_array += ["--mcp-config", @mcp_config] if @mcp_config
264
271
 
@@ -278,10 +285,18 @@ module ClaudeSwarm
278
285
  if @vibe
279
286
  cmd_array << "--dangerously-skip-permissions"
280
287
  else
288
+ # Build allowed tools list including MCP connections
289
+ allowed_tools = options[:allowed_tools] ? Array(options[:allowed_tools]).dup : []
290
+
291
+ # Add mcp__instance_name for each connection if we have instance info
292
+ options[:connections]&.each do |connection_name|
293
+ allowed_tools << "mcp__#{connection_name}"
294
+ end
295
+
281
296
  # Add allowed tools if any
282
- if options[:allowed_tools]
283
- tools = Array(options[:allowed_tools]).join(",")
284
- cmd_array += ["--allowedTools", tools]
297
+ if allowed_tools.any?
298
+ tools_str = allowed_tools.join(",")
299
+ cmd_array += ["--allowedTools", tools_str]
285
300
  end
286
301
 
287
302
  # Add disallowed tools if any
@@ -289,9 +304,6 @@ module ClaudeSwarm
289
304
  disallowed_tools = Array(options[:disallowed_tools]).join(",")
290
305
  cmd_array += ["--disallowedTools", disallowed_tools]
291
306
  end
292
-
293
- # Add permission prompt tool if not in vibe mode
294
- cmd_array += ["--permission-prompt-tool", "mcp__permissions__check_permission"]
295
307
  end
296
308
 
297
309
  cmd_array
@@ -28,7 +28,8 @@ module ClaudeSwarm
28
28
  instance_id: instance_config[:instance_id],
29
29
  calling_instance: calling_instance,
30
30
  calling_instance_id: calling_instance_id,
31
- claude_session_id: instance_config[:claude_session_id]
31
+ claude_session_id: instance_config[:claude_session_id],
32
+ additional_directories: instance_config[:directories][1..] || []
32
33
  )
33
34
 
34
35
  # Set class variables so tools can access them
@@ -6,7 +6,6 @@ require_relative "configuration"
6
6
  require_relative "mcp_generator"
7
7
  require_relative "orchestrator"
8
8
  require_relative "claude_mcp_server"
9
- require_relative "permission_mcp_server"
10
9
 
11
10
  module ClaudeSwarm
12
11
  class CLI < Thor
@@ -72,16 +71,20 @@ module ClaudeSwarm
72
71
  desc: "Instance name"
73
72
  method_option :directory, aliases: "-d", type: :string, required: true,
74
73
  desc: "Working directory for the instance"
74
+ method_option :directories, type: :array,
75
+ desc: "All directories (including main directory) for the instance"
75
76
  method_option :model, aliases: "-m", type: :string, required: true,
76
77
  desc: "Claude model to use (e.g., opus, sonnet)"
77
78
  method_option :prompt, aliases: "-p", type: :string,
78
79
  desc: "System prompt for the instance"
79
80
  method_option :description, type: :string,
80
81
  desc: "Description of the instance's role"
81
- method_option :tools, aliases: "-t", type: :array,
82
- desc: "Allowed tools for the instance"
82
+ method_option :allowed_tools, aliases: "-t", type: :array,
83
+ desc: "Allowed tools for the instance"
83
84
  method_option :disallowed_tools, type: :array,
84
85
  desc: "Disallowed tools for the instance"
86
+ method_option :connections, type: :array,
87
+ desc: "Connections to other instances"
85
88
  method_option :mcp_config_path, type: :string,
86
89
  desc: "Path to MCP configuration file"
87
90
  method_option :debug, type: :boolean, default: false,
@@ -100,13 +103,15 @@ module ClaudeSwarm
100
103
  instance_config = {
101
104
  name: options[:name],
102
105
  directory: options[:directory],
106
+ directories: options[:directories] || [options[:directory]],
103
107
  model: options[:model],
104
108
  prompt: options[:prompt],
105
109
  description: options[:description],
106
- tools: options[:tools] || [],
110
+ allowed_tools: options[:allowed_tools] || [],
107
111
  disallowed_tools: options[:disallowed_tools] || [],
112
+ connections: options[:connections] || [],
108
113
  mcp_config_path: options[:mcp_config_path],
109
- vibe: options[:vibe],
114
+ vibe: options[:vibe] || false,
110
115
  instance_id: options[:instance_id],
111
116
  claude_session_id: options[:claude_session_id]
112
117
  }
@@ -192,6 +197,82 @@ module ClaudeSwarm
192
197
  say "Claude Swarm #{VERSION}"
193
198
  end
194
199
 
200
+ desc "ps", "List running Claude Swarm sessions"
201
+ def ps
202
+ require_relative "commands/ps"
203
+ Commands::Ps.new.execute
204
+ end
205
+
206
+ desc "show SESSION_ID", "Show detailed session information"
207
+ def show(session_id)
208
+ require_relative "commands/show"
209
+ Commands::Show.new.execute(session_id)
210
+ end
211
+
212
+ desc "clean", "Remove stale session symlinks"
213
+ method_option :days, aliases: "-d", type: :numeric, default: 7,
214
+ desc: "Remove sessions older than N days"
215
+ def clean
216
+ run_dir = File.expand_path("~/.claude-swarm/run")
217
+ unless Dir.exist?(run_dir)
218
+ say "No run directory found", :yellow
219
+ return
220
+ end
221
+
222
+ cleaned = 0
223
+ Dir.glob("#{run_dir}/*").each do |symlink|
224
+ next unless File.symlink?(symlink)
225
+
226
+ begin
227
+ # Remove if target doesn't exist (stale)
228
+ unless File.exist?(File.readlink(symlink))
229
+ File.unlink(symlink)
230
+ cleaned += 1
231
+ next
232
+ end
233
+
234
+ # Remove if older than specified days
235
+ if File.stat(symlink).mtime < Time.now - (options[:days] * 86_400)
236
+ File.unlink(symlink)
237
+ cleaned += 1
238
+ end
239
+ rescue StandardError
240
+ # Skip problematic symlinks
241
+ end
242
+ end
243
+
244
+ say "Cleaned #{cleaned} stale session#{cleaned == 1 ? "" : "s"}", :green
245
+ end
246
+
247
+ desc "watch SESSION_ID", "Watch session logs"
248
+ method_option :lines, aliases: "-n", type: :numeric, default: 100,
249
+ desc: "Number of lines to show initially"
250
+ def watch(session_id)
251
+ # Find session path
252
+ run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
253
+ session_path = if File.symlink?(run_symlink)
254
+ File.readlink(run_symlink)
255
+ else
256
+ # Search in sessions directory
257
+ Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
258
+ File.basename(path) == session_id
259
+ end
260
+ end
261
+
262
+ unless session_path && Dir.exist?(session_path)
263
+ error "Session not found: #{session_id}"
264
+ exit 1
265
+ end
266
+
267
+ log_file = File.join(session_path, "session.log")
268
+ unless File.exist?(log_file)
269
+ error "Log file not found for session: #{session_id}"
270
+ exit 1
271
+ end
272
+
273
+ exec("tail", "-f", "-n", options[:lines].to_s, log_file)
274
+ end
275
+
195
276
  desc "list-sessions", "List all available Claude Swarm sessions"
196
277
  method_option :limit, aliases: "-l", type: :numeric, default: 10,
197
278
  desc: "Maximum number of sessions to display"
@@ -265,22 +346,6 @@ module ClaudeSwarm
265
346
  say " claude-swarm --session-id <session-id>", :cyan
266
347
  end
267
348
 
268
- desc "tools-mcp", "Start a permission management MCP server for tool access control"
269
- method_option :allowed_tools, aliases: "-t", type: :string,
270
- desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
271
- method_option :disallowed_tools, type: :string,
272
- desc: "Comma-separated list of disallowed tool patterns (supports wildcards)"
273
- method_option :debug, type: :boolean, default: false,
274
- desc: "Enable debug output"
275
- def tools_mcp
276
- server = PermissionMcpServer.new(allowed_tools: options[:allowed_tools], disallowed_tools: options[:disallowed_tools])
277
- server.start
278
- rescue StandardError => e
279
- error "Error starting permission MCP server: #{e.message}"
280
- error e.backtrace.join("\n") if options[:debug]
281
- exit 1
282
- end
283
-
284
349
  default_task :start
285
350
 
286
351
  private
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "time"
6
+
7
+ module ClaudeSwarm
8
+ module Commands
9
+ class Ps
10
+ RUN_DIR = File.expand_path("~/.claude-swarm/run")
11
+
12
+ def execute
13
+ unless Dir.exist?(RUN_DIR)
14
+ puts "No active sessions"
15
+ return
16
+ end
17
+
18
+ sessions = []
19
+
20
+ # Read all symlinks in run directory
21
+ Dir.glob("#{RUN_DIR}/*").each do |symlink|
22
+ next unless File.symlink?(symlink)
23
+
24
+ begin
25
+ session_dir = File.readlink(symlink)
26
+ # Skip if target doesn't exist (stale symlink)
27
+ next unless Dir.exist?(session_dir)
28
+
29
+ session_info = parse_session_info(session_dir)
30
+ sessions << session_info if session_info
31
+ rescue StandardError
32
+ # Skip problematic symlinks
33
+ end
34
+ end
35
+
36
+ if sessions.empty?
37
+ puts "No active sessions"
38
+ return
39
+ end
40
+
41
+ # Column widths
42
+ col_session = 15
43
+ col_swarm = 25
44
+ col_cost = 12
45
+ col_uptime = 10
46
+
47
+ # Display header with proper spacing
48
+ header = "#{
49
+ "SESSION_ID".ljust(col_session)
50
+ } #{
51
+ "SWARM_NAME".ljust(col_swarm)
52
+ } #{
53
+ "TOTAL_COST".ljust(col_cost)
54
+ } #{
55
+ "UPTIME".ljust(col_uptime)
56
+ } DIRECTORY"
57
+ puts "\n⚠️ \e[3mTotal cost does not include the cost of the main instance\e[0m\n\n"
58
+ puts header
59
+ puts "-" * header.length
60
+
61
+ # Display sessions sorted by start time (newest first)
62
+ sessions.sort_by { |s| s[:start_time] }.reverse.each do |session|
63
+ cost_str = format("$%.4f", session[:cost])
64
+ puts "#{
65
+ session[:id].ljust(col_session)
66
+ } #{
67
+ truncate(session[:name], col_swarm).ljust(col_swarm)
68
+ } #{
69
+ cost_str.ljust(col_cost)
70
+ } #{
71
+ session[:uptime].ljust(col_uptime)
72
+ } #{session[:directory]}"
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def parse_session_info(session_dir)
79
+ session_id = File.basename(session_dir)
80
+
81
+ # Load config for swarm name and main directory
82
+ config_file = File.join(session_dir, "config.yml")
83
+ return nil unless File.exist?(config_file)
84
+
85
+ config = YAML.load_file(config_file)
86
+ swarm_name = config.dig("swarm", "name") || "Unknown"
87
+ main_instance = config.dig("swarm", "main")
88
+
89
+ # Get all directories - handle both string and array formats
90
+ dir_config = config.dig("swarm", "instances", main_instance, "directory")
91
+ directories = if dir_config.is_a?(Array)
92
+ dir_config
93
+ else
94
+ [dir_config || "."]
95
+ end
96
+ directories_str = directories.join(", ")
97
+
98
+ # Calculate total cost from JSON log
99
+ total_cost = calculate_total_cost(session_dir)
100
+
101
+ # Get uptime from directory creation time
102
+ start_time = File.stat(session_dir).ctime
103
+ uptime = format_duration(Time.now - start_time)
104
+
105
+ {
106
+ id: session_id,
107
+ name: swarm_name,
108
+ cost: total_cost,
109
+ uptime: uptime,
110
+ directory: directories_str,
111
+ start_time: start_time
112
+ }
113
+ rescue StandardError
114
+ nil
115
+ end
116
+
117
+ def calculate_total_cost(session_dir)
118
+ log_file = File.join(session_dir, "session.log.json")
119
+ return 0.0 unless File.exist?(log_file)
120
+
121
+ total = 0.0
122
+ File.foreach(log_file) do |line|
123
+ data = JSON.parse(line)
124
+ total += data["event"]["total_cost_usd"] if data.dig("event", "type") == "result" && data.dig("event", "total_cost_usd")
125
+ rescue JSON::ParserError
126
+ next
127
+ end
128
+ total
129
+ end
130
+
131
+ def format_duration(seconds)
132
+ if seconds < 60
133
+ "#{seconds.to_i}s"
134
+ elsif seconds < 3600
135
+ "#{(seconds / 60).to_i}m"
136
+ elsif seconds < 86_400
137
+ "#{(seconds / 3600).to_i}h"
138
+ else
139
+ "#{(seconds / 86_400).to_i}d"
140
+ end
141
+ end
142
+
143
+ def truncate(str, length)
144
+ str.length > length ? "#{str[0...length - 2]}.." : str
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+
6
+ module ClaudeSwarm
7
+ module Commands
8
+ class Show
9
+ def execute(session_id)
10
+ session_path = find_session_path(session_id)
11
+ unless session_path
12
+ puts "Session not found: #{session_id}"
13
+ exit 1
14
+ end
15
+
16
+ # Load config to get main instance name
17
+ config = YAML.load_file(File.join(session_path, "config.yml"))
18
+ main_instance_name = config.dig("swarm", "main")
19
+
20
+ # Parse all events to build instance data
21
+ instances = parse_instance_hierarchy(session_path, main_instance_name)
22
+
23
+ # Calculate total cost (excluding main if not available)
24
+ total_cost = instances.values.sum { |i| i[:cost] }
25
+ cost_display = if instances[main_instance_name] && instances[main_instance_name][:has_cost_data]
26
+ format("$%.4f", total_cost)
27
+ else
28
+ "#{format("$%.4f", total_cost)} (excluding main instance)"
29
+ end
30
+
31
+ # Display session info
32
+ puts "Session: #{session_id}"
33
+ puts "Swarm: #{config.dig("swarm", "name")}"
34
+ puts "Total Cost: #{cost_display}"
35
+
36
+ # Try to read start directory
37
+ start_dir_file = File.join(session_path, "start_directory")
38
+ puts "Start Directory: #{File.read(start_dir_file).strip}" if File.exist?(start_dir_file)
39
+
40
+ puts
41
+ puts "Instance Hierarchy:"
42
+ puts "-" * 50
43
+
44
+ # Find root instances
45
+ roots = instances.values.select { |i| i[:called_by].empty? }
46
+ roots.each do |instance|
47
+ display_instance_tree(instance, instances, 0, main_instance_name)
48
+ end
49
+
50
+ # Add note about interactive main instance
51
+ return if instances[main_instance_name]&.dig(:has_cost_data)
52
+
53
+ puts
54
+ puts "Note: Main instance (#{main_instance_name}) cost is not tracked in interactive mode."
55
+ puts " View costs directly in the Claude interface."
56
+ end
57
+
58
+ private
59
+
60
+ def find_session_path(session_id)
61
+ # First check the run directory
62
+ run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
63
+ if File.symlink?(run_symlink)
64
+ target = File.readlink(run_symlink)
65
+ return target if Dir.exist?(target)
66
+ end
67
+
68
+ # Fall back to searching all sessions
69
+ Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
70
+ File.basename(path) == session_id
71
+ end
72
+ end
73
+
74
+ def parse_instance_hierarchy(session_path, _main_instance_name)
75
+ log_file = File.join(session_path, "session.log.json")
76
+ instances = {}
77
+
78
+ return instances unless File.exist?(log_file)
79
+
80
+ File.foreach(log_file) do |line|
81
+ data = JSON.parse(line)
82
+ instance_name = data["instance"]
83
+ instance_id = data["instance_id"]
84
+ calling_instance = data["calling_instance"]
85
+
86
+ # Initialize instance data
87
+ instances[instance_name] ||= {
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
123
+ end
124
+
125
+ instances
126
+ end
127
+
128
+ def display_instance_tree(instance, all_instances, level, main_instance_name)
129
+ indent = " " * level
130
+ prefix = level.zero? ? "├─" : "└─"
131
+
132
+ # Display instance name with special marker for main
133
+ instance_display = instance[:name]
134
+ instance_display += " [main]" if instance[:name] == main_instance_name
135
+
136
+ puts "#{indent}#{prefix} #{instance_display} (#{instance[:id]})"
137
+
138
+ # Display cost - show n/a for main instance without cost data
139
+ cost_display = if instance[:name] == main_instance_name && !instance[:has_cost_data]
140
+ "n/a (interactive)"
141
+ else
142
+ format("$%.4f", instance[:cost])
143
+ end
144
+
145
+ puts "#{indent} Cost: #{cost_display} | Calls: #{instance[:calls]}"
146
+
147
+ # Display children
148
+ children = instance[:calls_to].map { |name| all_instances[name] }.compact
149
+ children.each do |child|
150
+ # Don't recurse if we've already shown this instance (avoid cycles)
151
+ next if level.positive? && child[:called_by].size > 1
152
+
153
+ display_instance_tree(child, all_instances, level + 1, main_instance_name)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -86,9 +86,13 @@ module ClaudeSwarm
86
86
  # Support both 'tools' (deprecated) and 'allowed_tools' for backward compatibility
87
87
  allowed_tools = config["allowed_tools"] || config["tools"] || []
88
88
 
89
+ # Parse directory field - support both string and array
90
+ directories = parse_directories(config["directory"])
91
+
89
92
  {
90
93
  name: name,
91
- directory: expand_path(config["directory"] || "."),
94
+ directory: directories.first, # Keep single directory for backward compatibility
95
+ directories: directories, # New field with all directories
92
96
  model: config["model"] || "sonnet",
93
97
  connections: Array(config["connections"]),
94
98
  tools: Array(allowed_tools), # Keep as 'tools' internally for compatibility
@@ -156,8 +160,10 @@ module ClaudeSwarm
156
160
 
157
161
  def validate_directories
158
162
  @instances.each do |name, instance|
159
- directory = instance[:directory]
160
- raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
163
+ # Validate all directories in the directories array
164
+ instance[:directories].each do |directory|
165
+ raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
166
+ end
161
167
  end
162
168
  end
163
169
 
@@ -168,6 +174,17 @@ module ClaudeSwarm
168
174
  raise Error, "Instance '#{instance_name}' field '#{field_name}' must be an array, got #{field_value.class.name}" unless field_value.is_a?(Array)
169
175
  end
170
176
 
177
+ def parse_directories(directory_config)
178
+ # Default to current directory if not specified
179
+ directory_config ||= "."
180
+
181
+ # Convert to array and expand paths
182
+ directories = Array(directory_config).map { |dir| expand_path(dir) }
183
+
184
+ # Ensure at least one directory
185
+ directories.empty? ? [expand_path(".")] : directories
186
+ end
187
+
171
188
  def expand_path(path)
172
189
  Pathname.new(path).expand_path(@base_dir).to_s
173
190
  end
@@ -68,9 +68,6 @@ module ClaudeSwarm
68
68
  )
69
69
  end
70
70
 
71
- # Add permission MCP server if not in vibe mode (global or instance-specific)
72
- mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools], instance[:disallowed_tools]) unless @vibe || instance[:vibe]
73
-
74
71
  config = {
75
72
  "instance_id" => @instance_ids[name],
76
73
  "instance_name" => name,
@@ -110,15 +107,20 @@ module ClaudeSwarm
110
107
  "--model", instance[:model]
111
108
  ]
112
109
 
110
+ # Add directories array if we have multiple directories
111
+ args.push("--directories", *instance[:directories]) if instance[:directories] && instance[:directories].size > 1
112
+
113
113
  # Add optional arguments
114
114
  args.push("--prompt", instance[:prompt]) if instance[:prompt]
115
115
 
116
116
  args.push("--description", instance[:description]) if instance[:description]
117
117
 
118
- args.push("--tools", instance[:tools].join(",")) if instance[:tools] && !instance[:tools].empty?
118
+ args.push("--allowed-tools", instance[:allowed_tools].join(",")) if instance[:allowed_tools] && !instance[:allowed_tools].empty?
119
119
 
120
120
  args.push("--disallowed-tools", instance[:disallowed_tools].join(",")) if instance[:disallowed_tools] && !instance[:disallowed_tools].empty?
121
121
 
122
+ args.push("--connections", instance[:connections].join(",")) if instance[:connections] && !instance[:connections].empty?
123
+
122
124
  args.push("--mcp-config-path", mcp_config_path(name))
123
125
 
124
126
  args.push("--calling-instance", calling_instance) if calling_instance
@@ -162,23 +164,5 @@ module ClaudeSwarm
162
164
  # Skip invalid state files
163
165
  end
164
166
  end
165
-
166
- def build_permission_mcp_config(allowed_tools, disallowed_tools)
167
- exe_path = "claude-swarm"
168
-
169
- args = ["tools-mcp"]
170
-
171
- # Add allowed tools if specified
172
- args.push("--allowed-tools", allowed_tools.join(",")) if allowed_tools && !allowed_tools.empty?
173
-
174
- # Add disallowed tools if specified
175
- args.push("--disallowed-tools", disallowed_tools.join(",")) if disallowed_tools && !disallowed_tools.empty?
176
-
177
- {
178
- "type" => "stdio",
179
- "command" => exe_path,
180
- "args" => args
181
- }
182
- end
183
167
  end
184
168
  end