claude_swarm 0.1.11 → 0.1.13

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
+ require "json"
4
5
  require_relative "configuration"
5
6
  require_relative "mcp_generator"
6
7
  require_relative "orchestrator"
@@ -24,7 +25,15 @@ module ClaudeSwarm
24
25
  desc: "Stream session logs to stdout (only works with -p)"
25
26
  method_option :debug, type: :boolean, default: false,
26
27
  desc: "Enable debug output"
28
+ method_option :session_id, type: :string,
29
+ desc: "Resume a previous session by ID or path"
27
30
  def start(config_file = nil)
31
+ # Handle session restoration
32
+ if options[:session_id]
33
+ restore_session(options[:session_id])
34
+ return
35
+ end
36
+
28
37
  config_path = config_file || options[:config]
29
38
  unless File.exist?(config_path)
30
39
  error "Configuration file not found: #{config_path}"
@@ -41,12 +50,10 @@ module ClaudeSwarm
41
50
 
42
51
  begin
43
52
  config = Configuration.new(config_path)
44
- session_timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
45
- generator = McpGenerator.new(config, vibe: options[:vibe], timestamp: session_timestamp)
53
+ generator = McpGenerator.new(config, vibe: options[:vibe])
46
54
  orchestrator = Orchestrator.new(config, generator,
47
55
  vibe: options[:vibe],
48
56
  prompt: options[:prompt],
49
- session_timestamp: session_timestamp,
50
57
  stream_logs: options[:stream_logs],
51
58
  debug: options[:debug])
52
59
  orchestrator.start
@@ -83,6 +90,12 @@ module ClaudeSwarm
83
90
  desc: "Run with --dangerously-skip-permissions"
84
91
  method_option :calling_instance, type: :string, required: true,
85
92
  desc: "Name of the instance that launched this MCP server"
93
+ method_option :calling_instance_id, type: :string,
94
+ desc: "Unique ID of the instance that launched this MCP server"
95
+ method_option :instance_id, type: :string,
96
+ desc: "Unique ID of this instance"
97
+ method_option :claude_session_id, type: :string,
98
+ desc: "Claude session ID to resume"
86
99
  def mcp_serve
87
100
  instance_config = {
88
101
  name: options[:name],
@@ -93,11 +106,17 @@ module ClaudeSwarm
93
106
  tools: options[:tools] || [],
94
107
  disallowed_tools: options[:disallowed_tools] || [],
95
108
  mcp_config_path: options[:mcp_config_path],
96
- vibe: options[:vibe]
109
+ vibe: options[:vibe],
110
+ instance_id: options[:instance_id],
111
+ claude_session_id: options[:claude_session_id]
97
112
  }
98
113
 
99
114
  begin
100
- server = ClaudeMcpServer.new(instance_config, calling_instance: options[:calling_instance])
115
+ server = ClaudeMcpServer.new(
116
+ instance_config,
117
+ calling_instance: options[:calling_instance],
118
+ calling_instance_id: options[:calling_instance_id]
119
+ )
101
120
  server.start
102
121
  rescue StandardError => e
103
122
  error "Error starting MCP server: #{e.message}"
@@ -173,6 +192,79 @@ module ClaudeSwarm
173
192
  say "Claude Swarm #{VERSION}"
174
193
  end
175
194
 
195
+ desc "list-sessions", "List all available Claude Swarm sessions"
196
+ method_option :limit, aliases: "-l", type: :numeric, default: 10,
197
+ desc: "Maximum number of sessions to display"
198
+ def list_sessions
199
+ sessions_dir = File.expand_path("~/.claude-swarm/sessions")
200
+ unless Dir.exist?(sessions_dir)
201
+ say "No sessions found", :yellow
202
+ return
203
+ end
204
+
205
+ # Find all sessions with MCP configs
206
+ sessions = []
207
+ Dir.glob("#{sessions_dir}/*/*/*.mcp.json").each do |mcp_path|
208
+ session_path = File.dirname(mcp_path)
209
+ session_id = File.basename(session_path)
210
+ project_name = File.basename(File.dirname(session_path))
211
+
212
+ # Skip if we've already processed this session
213
+ next if sessions.any? { |s| s[:path] == session_path }
214
+
215
+ # Try to load session info
216
+ config_file = File.join(session_path, "config.yml")
217
+ next unless File.exist?(config_file)
218
+
219
+ # Load the config to get swarm info
220
+ config_data = YAML.load_file(config_file)
221
+ swarm_name = config_data.dig("swarm", "name") || "Unknown"
222
+ main_instance = config_data.dig("swarm", "main") || "Unknown"
223
+
224
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
225
+
226
+ # Get creation time from directory
227
+ created_at = File.stat(session_path).ctime
228
+
229
+ sessions << {
230
+ path: session_path,
231
+ id: session_id,
232
+ project: project_name,
233
+ created_at: created_at,
234
+ main_instance: main_instance,
235
+ instances_count: mcp_files.size,
236
+ swarm_name: swarm_name,
237
+ config_path: config_file
238
+ }
239
+ rescue StandardError
240
+ # Skip invalid manifests
241
+ next
242
+ end
243
+
244
+ if sessions.empty?
245
+ say "No sessions found", :yellow
246
+ return
247
+ end
248
+
249
+ # Sort by creation time (newest first)
250
+ sessions.sort_by! { |s| -s[:created_at].to_i }
251
+ sessions = sessions.first(options[:limit])
252
+
253
+ # Display sessions
254
+ say "\nAvailable sessions (newest first):\n", :bold
255
+ sessions.each do |session|
256
+ say "\n#{session[:project]}/#{session[:id]}", :green
257
+ say " Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}"
258
+ say " Main: #{session[:main_instance]}"
259
+ say " Instances: #{session[:instances_count]}"
260
+ say " Swarm: #{session[:swarm_name]}"
261
+ say " Config: #{session[:config_path]}", :cyan
262
+ end
263
+
264
+ say "\nTo resume a session, run:", :bold
265
+ say " claude-swarm --session-id <session-id>", :cyan
266
+ end
267
+
176
268
  desc "tools-mcp", "Start a permission management MCP server for tool access control"
177
269
  method_option :allowed_tools, aliases: "-t", type: :string,
178
270
  desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
@@ -196,5 +288,77 @@ module ClaudeSwarm
196
288
  def error(message)
197
289
  say message, :red
198
290
  end
291
+
292
+ def restore_session(session_id)
293
+ say "Restoring session: #{session_id}", :green
294
+
295
+ # Find the session path
296
+ session_path = find_session_path(session_id)
297
+ unless session_path
298
+ error "Session not found: #{session_id}"
299
+ exit 1
300
+ end
301
+
302
+ begin
303
+ # Load session info from instance ID in MCP config
304
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
305
+ if mcp_files.empty?
306
+ error "No MCP configuration files found in session"
307
+ exit 1
308
+ end
309
+
310
+ # Load the configuration from the session directory
311
+ config_file = File.join(session_path, "config.yml")
312
+
313
+ unless File.exist?(config_file)
314
+ error "Configuration file not found in session"
315
+ exit 1
316
+ end
317
+
318
+ # Change to the original start directory if it exists
319
+ start_dir_file = File.join(session_path, "start_directory")
320
+ if File.exist?(start_dir_file)
321
+ original_dir = File.read(start_dir_file).strip
322
+ if Dir.exist?(original_dir)
323
+ Dir.chdir(original_dir)
324
+ say "Changed to original directory: #{original_dir}", :green unless options[:prompt]
325
+ else
326
+ error "Original directory no longer exists: #{original_dir}"
327
+ exit 1
328
+ end
329
+ end
330
+
331
+ config = Configuration.new(config_file, base_dir: Dir.pwd)
332
+
333
+ # Create orchestrator with restoration mode
334
+ generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
335
+ orchestrator = Orchestrator.new(config, generator,
336
+ vibe: options[:vibe],
337
+ prompt: options[:prompt],
338
+ stream_logs: options[:stream_logs],
339
+ debug: options[:debug],
340
+ restore_session_path: session_path)
341
+ orchestrator.start
342
+ rescue StandardError => e
343
+ error "Failed to restore session: #{e.message}"
344
+ error e.backtrace.join("\n") if options[:debug]
345
+ exit 1
346
+ end
347
+ end
348
+
349
+ def find_session_path(session_id)
350
+ sessions_dir = File.expand_path("~/.claude-swarm/sessions")
351
+
352
+ # Check if it's a full path
353
+ return session_id if File.exist?(File.join(session_id, "config.yml"))
354
+
355
+ # Search for the session ID in all projects
356
+ Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
357
+ config_path = File.join(path, "config.yml")
358
+ return path if File.exist?(config_path)
359
+ end
360
+
361
+ nil
362
+ end
199
363
  end
200
364
  end
@@ -5,11 +5,11 @@ require "pathname"
5
5
 
6
6
  module ClaudeSwarm
7
7
  class Configuration
8
- attr_reader :config, :swarm_name, :main_instance, :instances
8
+ attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
9
9
 
10
- def initialize(config_path)
10
+ def initialize(config_path, base_dir: nil)
11
11
  @config_path = Pathname.new(config_path).expand_path
12
- @config_dir = @config_path.dirname
12
+ @config_dir = base_dir || @config_path.dirname
13
13
  load_and_validate
14
14
  end
15
15
 
@@ -60,14 +60,15 @@ module ClaudeSwarm
60
60
  end
61
61
 
62
62
  def parse_swarm
63
- swarm = @config["swarm"]
64
- @swarm_name = swarm["name"]
65
- @main_instance = swarm["main"]
63
+ @swarm = @config["swarm"]
64
+ @swarm_name = @swarm["name"]
65
+ @main_instance = @swarm["main"]
66
66
  @instances = {}
67
- swarm["instances"].each do |name, config|
67
+ @swarm["instances"].each do |name, config|
68
68
  @instances[name] = parse_instance(name, config)
69
69
  end
70
70
  validate_connections
71
+ detect_circular_dependencies
71
72
  end
72
73
 
73
74
  def parse_instance(name, config)
@@ -127,6 +128,31 @@ module ClaudeSwarm
127
128
  end
128
129
  end
129
130
 
131
+ def detect_circular_dependencies
132
+ @instances.each_key do |instance_name|
133
+ visited = Set.new
134
+ path = []
135
+ detect_cycle_from(instance_name, visited, path)
136
+ end
137
+ end
138
+
139
+ def detect_cycle_from(instance_name, visited, path)
140
+ return if visited.include?(instance_name)
141
+
142
+ if path.include?(instance_name)
143
+ cycle_start = path.index(instance_name)
144
+ cycle = path[cycle_start..] + [instance_name]
145
+ raise Error, "Circular dependency detected: #{cycle.join(" -> ")}"
146
+ end
147
+
148
+ path.push(instance_name)
149
+ @instances[instance_name][:connections].each do |connection|
150
+ detect_cycle_from(connection, visited, path)
151
+ end
152
+ path.pop
153
+ visited.add(instance_name)
154
+ end
155
+
130
156
  def validate_directories
131
157
  @instances.each do |name, instance|
132
158
  directory = instance[:directory]
@@ -3,45 +3,52 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require "shellwords"
6
+ require "securerandom"
7
+ require_relative "session_path"
6
8
 
7
9
  module ClaudeSwarm
8
10
  class McpGenerator
9
- SWARM_DIR = ".claude-swarm"
10
- SESSIONS_SUBDIR = "sessions"
11
-
12
- def initialize(configuration, vibe: false, timestamp: nil)
11
+ def initialize(configuration, vibe: false, restore_session_path: nil)
13
12
  @config = configuration
14
13
  @vibe = vibe
15
- @timestamp = timestamp || Time.now.strftime("%Y%m%d_%H%M%S")
14
+ @restore_session_path = restore_session_path
15
+ @session_path = nil # Will be set when needed
16
+ @instance_ids = {} # Store instance IDs for all instances
17
+ @restore_states = {} # Store loaded state data during restoration
16
18
  end
17
19
 
18
20
  def generate_all
19
21
  ensure_swarm_directory
20
22
 
23
+ if @restore_session_path
24
+ # Load existing instance IDs and states from state files
25
+ load_instance_states
26
+ else
27
+ # Generate new instance IDs
28
+ @config.instances.each_key do |name|
29
+ @instance_ids[name] = "#{name}_#{SecureRandom.hex(4)}"
30
+ end
31
+ end
32
+
21
33
  @config.instances.each do |name, instance|
22
34
  generate_mcp_config(name, instance)
23
35
  end
24
36
  end
25
37
 
26
38
  def mcp_config_path(instance_name)
27
- File.join(Dir.pwd, SWARM_DIR, SESSIONS_SUBDIR, @timestamp, "#{instance_name}.mcp.json")
39
+ File.join(session_path, "#{instance_name}.mcp.json")
28
40
  end
29
41
 
30
42
  private
31
43
 
32
- def swarm_dir
33
- File.join(Dir.pwd, SWARM_DIR)
44
+ def session_path
45
+ @session_path ||= SessionPath.from_env
34
46
  end
35
47
 
36
48
  def ensure_swarm_directory
37
- FileUtils.mkdir_p(swarm_dir)
38
-
39
- # Create session directory with timestamp
40
- session_dir = File.join(swarm_dir, SESSIONS_SUBDIR, @timestamp)
41
- FileUtils.mkdir_p(session_dir)
42
-
43
- gitignore_path = File.join(swarm_dir, ".gitignore")
44
- File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
49
+ # Session directory is already created by orchestrator
50
+ # Just ensure it exists
51
+ SessionPath.ensure_directory(session_path)
45
52
  end
46
53
 
47
54
  def generate_mcp_config(name, instance)
@@ -55,13 +62,18 @@ module ClaudeSwarm
55
62
  # Add connection MCPs for other instances
56
63
  instance[:connections].each do |connection_name|
57
64
  connected_instance = @config.instances[connection_name]
58
- mcp_servers[connection_name] = build_instance_mcp_config(connection_name, connected_instance, calling_instance: name)
65
+ mcp_servers[connection_name] = build_instance_mcp_config(
66
+ connection_name, connected_instance,
67
+ calling_instance: name, calling_instance_id: @instance_ids[name]
68
+ )
59
69
  end
60
70
 
61
71
  # Add permission MCP server if not in vibe mode (global or instance-specific)
62
72
  mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools], instance[:disallowed_tools]) unless @vibe || instance[:vibe]
63
73
 
64
74
  config = {
75
+ "instance_id" => @instance_ids[name],
76
+ "instance_name" => name,
65
77
  "mcpServers" => mcp_servers
66
78
  }
67
79
 
@@ -86,7 +98,7 @@ module ClaudeSwarm
86
98
  end
87
99
  end
88
100
 
89
- def build_instance_mcp_config(name, instance, calling_instance:)
101
+ def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
90
102
  # Get the path to the claude-swarm executable
91
103
  exe_path = "claude-swarm"
92
104
 
@@ -111,8 +123,18 @@ module ClaudeSwarm
111
123
 
112
124
  args.push("--calling-instance", calling_instance) if calling_instance
113
125
 
126
+ args.push("--calling-instance-id", calling_instance_id) if calling_instance_id
127
+
128
+ args.push("--instance-id", @instance_ids[name]) if @instance_ids[name]
129
+
114
130
  args.push("--vibe") if @vibe || instance[:vibe]
115
131
 
132
+ # Add claude session ID if restoring
133
+ if @restore_states[name.to_s]
134
+ claude_session_id = @restore_states[name.to_s]["claude_session_id"]
135
+ args.push("--claude-session-id", claude_session_id) if claude_session_id
136
+ end
137
+
116
138
  {
117
139
  "type" => "stdio",
118
140
  "command" => exe_path,
@@ -120,6 +142,27 @@ module ClaudeSwarm
120
142
  }
121
143
  end
122
144
 
145
+ def load_instance_states
146
+ state_dir = File.join(@restore_session_path, "state")
147
+ return unless Dir.exist?(state_dir)
148
+
149
+ Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
150
+ data = JSON.parse(File.read(state_file))
151
+ instance_name = data["instance_name"]
152
+ instance_id = data["instance_id"]
153
+
154
+ # Check both string and symbol keys since config instances might have either
155
+ if instance_name && (@config.instances.key?(instance_name) || @config.instances.key?(instance_name.to_sym))
156
+ # Store with the same key type as in @config.instances
157
+ key = @config.instances.key?(instance_name) ? instance_name : instance_name.to_sym
158
+ @instance_ids[key] = instance_id
159
+ @restore_states[instance_name] = data
160
+ end
161
+ rescue StandardError
162
+ # Skip invalid state files
163
+ end
164
+ end
165
+
123
166
  def build_permission_mcp_config(allowed_tools, disallowed_tools)
124
167
  exe_path = "claude-swarm"
125
168
 
@@ -134,10 +177,7 @@ module ClaudeSwarm
134
177
  {
135
178
  "type" => "stdio",
136
179
  "command" => exe_path,
137
- "args" => args,
138
- "env" => {
139
- "CLAUDE_SWARM_SESSION_TIMESTAMP" => @timestamp
140
- }
180
+ "args" => args
141
181
  }
142
182
  end
143
183
  end
@@ -1,38 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "shellwords"
4
+ require "json"
5
+ require "fileutils"
6
+ require_relative "session_path"
7
+ require_relative "process_tracker"
4
8
 
5
9
  module ClaudeSwarm
6
10
  class Orchestrator
7
- def initialize(configuration, mcp_generator, vibe: false, prompt: nil, session_timestamp: nil, stream_logs: false, debug: false)
11
+ def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
12
+ restore_session_path: nil)
8
13
  @config = configuration
9
14
  @generator = mcp_generator
10
15
  @vibe = vibe
11
16
  @prompt = prompt
12
- @session_timestamp = session_timestamp || Time.now.strftime("%Y%m%d_%H%M%S")
13
17
  @stream_logs = stream_logs
14
18
  @debug = debug
19
+ @restore_session_path = restore_session_path
15
20
  end
16
21
 
17
22
  def start
18
- unless @prompt
19
- puts "šŸ Starting Claude Swarm: #{@config.swarm_name}"
20
- puts "šŸ˜Ž Vibe mode ON" if @vibe
21
- puts
22
- end
23
+ if @restore_session_path
24
+ unless @prompt
25
+ puts "šŸ”„ Restoring Claude Swarm: #{@config.swarm_name}"
26
+ puts "šŸ˜Ž Vibe mode ON" if @vibe
27
+ puts
28
+ end
23
29
 
24
- # Set session timestamp for all instances to share the same session directory
25
- ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] = @session_timestamp
26
- unless @prompt
27
- puts "šŸ“ Session files will be saved to: .claude-swarm/sessions/#{@session_timestamp}/"
28
- puts
29
- end
30
+ # Use existing session path
31
+ session_path = @restore_session_path
32
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
33
+ ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
30
34
 
31
- # Generate all MCP configuration files
32
- @generator.generate_all
33
- unless @prompt
34
- puts "āœ“ Generated MCP configurations in session directory"
35
- puts
35
+ unless @prompt
36
+ puts "šŸ“ Using existing session: #{session_path}/"
37
+ puts
38
+ end
39
+
40
+ # Initialize process tracker
41
+ @process_tracker = ProcessTracker.new(session_path)
42
+
43
+ # Set up signal handlers to clean up child processes
44
+ setup_signal_handlers
45
+
46
+ # Regenerate MCP configurations with session IDs for restoration
47
+ @generator.generate_all
48
+ unless @prompt
49
+ puts "āœ“ Regenerated MCP configurations with session IDs"
50
+ puts
51
+ end
52
+ else
53
+ unless @prompt
54
+ puts "šŸ Starting Claude Swarm: #{@config.swarm_name}"
55
+ puts "šŸ˜Ž Vibe mode ON" if @vibe
56
+ puts
57
+ end
58
+
59
+ # Generate and set session path for all instances
60
+ session_path = SessionPath.generate(working_dir: Dir.pwd)
61
+ SessionPath.ensure_directory(session_path)
62
+
63
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
64
+ ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
65
+
66
+ unless @prompt
67
+ puts "šŸ“ Session files will be saved to: #{session_path}/"
68
+ puts
69
+ end
70
+
71
+ # Initialize process tracker
72
+ @process_tracker = ProcessTracker.new(session_path)
73
+
74
+ # Set up signal handlers to clean up child processes
75
+ setup_signal_handlers
76
+
77
+ # Generate all MCP configuration files
78
+ @generator.generate_all
79
+ unless @prompt
80
+ puts "āœ“ Generated MCP configurations in session directory"
81
+ puts
82
+ end
83
+
84
+ # Save swarm config path for restoration
85
+ save_swarm_config_path(session_path)
36
86
  end
37
87
 
38
88
  # Launch the main instance
@@ -64,19 +114,47 @@ module ClaudeSwarm
64
114
  end
65
115
 
66
116
  # Clean up log streaming thread
67
- return unless log_thread
117
+ if log_thread
118
+ log_thread.terminate
119
+ log_thread.join
120
+ end
68
121
 
69
- log_thread.terminate
70
- log_thread.join
122
+ # Clean up child processes
123
+ cleanup_processes
71
124
  end
72
125
 
73
126
  private
74
127
 
128
+ def save_swarm_config_path(session_path)
129
+ # Copy the YAML config file to the session directory
130
+ config_copy_path = File.join(session_path, "config.yml")
131
+ FileUtils.cp(@config.config_path, config_copy_path)
132
+
133
+ # Save the original working directory
134
+ start_dir_file = File.join(session_path, "start_directory")
135
+ File.write(start_dir_file, Dir.pwd)
136
+ end
137
+
138
+ def setup_signal_handlers
139
+ %w[INT TERM QUIT].each do |signal|
140
+ Signal.trap(signal) do
141
+ puts "\nšŸ›‘ Received #{signal} signal, cleaning up..."
142
+ cleanup_processes
143
+ exit
144
+ end
145
+ end
146
+ end
147
+
148
+ def cleanup_processes
149
+ @process_tracker.cleanup_all
150
+ puts "āœ“ Cleanup complete"
151
+ rescue StandardError => e
152
+ puts "āš ļø Error during cleanup: #{e.message}"
153
+ end
154
+
75
155
  def start_log_streaming
76
156
  Thread.new do
77
- session_log_path = File.join(Dir.pwd, ClaudeSwarm::ClaudeCodeExecutor::SWARM_DIR,
78
- ClaudeSwarm::ClaudeCodeExecutor::SESSIONS_DIR,
79
- @session_timestamp, "session.log")
157
+ session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
80
158
 
81
159
  # Wait for log file to be created
82
160
  sleep 0.1 until File.exist?(session_log_path)
@@ -107,6 +185,26 @@ module ClaudeSwarm
107
185
  instance[:model]
108
186
  ]
109
187
 
188
+ # Add resume flag if restoring session
189
+ if @restore_session_path
190
+ # Look for main instance state file
191
+ main_instance_name = @config.main_instance
192
+ state_files = Dir.glob(File.join(@restore_session_path, "state", "*.json"))
193
+
194
+ # Find the state file for the main instance
195
+ state_files.each do |state_file|
196
+ state_data = JSON.parse(File.read(state_file))
197
+ next unless state_data["instance_name"] == main_instance_name
198
+
199
+ claude_session_id = state_data["claude_session_id"]
200
+ if claude_session_id
201
+ parts << "--resume"
202
+ parts << claude_session_id
203
+ end
204
+ break
205
+ end
206
+ end
207
+
110
208
  if @vibe || instance[:vibe]
111
209
  parts << "--dangerously-skip-permissions"
112
210
  else
@@ -5,13 +5,11 @@ require "fast_mcp"
5
5
  require "logger"
6
6
  require "fileutils"
7
7
  require_relative "permission_tool"
8
+ require_relative "session_path"
9
+ require_relative "process_tracker"
8
10
 
9
11
  module ClaudeSwarm
10
12
  class PermissionMcpServer
11
- # Directory constants
12
- SWARM_DIR = ".claude-swarm"
13
- SESSIONS_DIR = "sessions"
14
-
15
13
  # Server configuration
16
14
  SERVER_NAME = "claude-swarm-permissions"
17
15
  SERVER_VERSION = "1.0.0"
@@ -49,6 +47,13 @@ module ClaudeSwarm
49
47
  end
50
48
 
51
49
  def create_and_start_server
50
+ # Track this process
51
+ session_path = SessionPath.from_env
52
+ if session_path && File.exist?(session_path)
53
+ tracker = ProcessTracker.new(session_path)
54
+ tracker.track_pid(Process.pid, "mcp_permissions")
55
+ end
56
+
52
57
  server = FastMcp::Server.new(
53
58
  name: SERVER_NAME,
54
59
  version: SERVER_VERSION
@@ -60,20 +65,14 @@ module ClaudeSwarm
60
65
  end
61
66
 
62
67
  def setup_logging
63
- session_dir = create_session_directory
64
- @logger = create_logger(session_dir)
68
+ session_path = SessionPath.from_env
69
+ SessionPath.ensure_directory(session_path)
70
+ @logger = create_logger(session_path)
65
71
  @logger.info("Permission MCP server logging initialized")
66
72
  end
67
73
 
68
- def create_session_directory
69
- session_timestamp = ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
70
- session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, session_timestamp)
71
- FileUtils.mkdir_p(session_dir)
72
- session_dir
73
- end
74
-
75
- def create_logger(session_dir)
76
- log_path = File.join(session_dir, "permissions.log")
74
+ def create_logger(session_path)
75
+ log_path = File.join(session_path, "permissions.log")
77
76
  logger = Logger.new(log_path)
78
77
  logger.level = Logger::DEBUG
79
78
  logger.formatter = log_formatter